
/* 
 * Argyll Color Correction System
 * Color Device profile checker.
 *
 * Author: Graeme W. Gill
 * Date:   15/7/2001
 *
 * Copyright 2001 - 2005 Graeme W. Gill
 * All rights reserved.
 *
 * This material is licenced under the GNU AFFERO GENERAL PUBLIC LICENSE Version 3 :-
 * see the License.txt file for licencing details.
 */

/*
 * This program takes in the .ti3 scattered test chart
 * points, and checks them against an ICC profile.
 * forward ICC device profile.
 */

/*
 * TTBD:
 *		Switch to generic colorant read code rather than Grey/RGB/CMYK,
 *		and allow checking ICC profiles > 4 colors
 */

#undef DEBUG

#undef HACK					/* Print per patch XYZ differences */

#define IMP_MONO			/* Turn on development code */

#define verbo stdout

#include <stdio.h>
#include <string.h>
#include <math.h>
#include <ctype.h>
#include "copyright.h"
#include "aconfig.h"
#include "numlib.h"
#include "cgats.h"
#include "xicc.h"
#include "insttypes.h"
#include "sort.h"
#include "plot.h"
#include "vrml.h"
#include "ui.h"

void
usage(void) {
	fprintf(stderr,"Check accuracy of ICC profile, Version %s\n",ARGYLL_VERSION_STR);
	fprintf(stderr,"Author: Graeme W. Gill, licensed under the AGPL Version 3\n");
	fprintf(stderr,"usage: profcheck [-options] data.ti3 iccprofile.icm\n");
	fprintf(stderr," -v [level]      Verbosity level (default 1), 2 to print each DE\n");
	fprintf(stderr," -c              Show CIE94 delta E values\n");
	fprintf(stderr," -k              Show CIEDE2000 delta E values\n");
	fprintf(stderr," -w              create %s visualisation (iccprofile%s)\n",vrml_format(),vrml_ext());
	fprintf(stderr," -x              Use %s axes\n",vrml_format());
	fprintf(stderr," -m              Make %s lines a minimum of 0.5\n",vrml_format());
	fprintf(stderr," -e              Color vectors acording to delta E\n");
	fprintf(stderr," -h              Plot a histogram of delta E's\n");
	fprintf(stderr," -s              Sort output by delta E\n");
	fprintf(stderr," -P N.NN         Create a pruned .ti3 with points less or equal to N.NN delta E\n");
	fprintf(stderr," -d devval1,deval2,devvalN\n");
	fprintf(stderr,"                 Specify a device value to sort against\n");
	fprintf(stderr," -p              Sort device value by PCS (Lab) target\n");
	fprintf(stderr," -f [illum]      Use Fluorescent Whitening Agent compensation [opt. simulated inst. illum.:\n");
	fprintf(stderr,"                  M0, M1, M2, A, C, D50 (def.), D50M2, D65, F5, F8, F10 or file.sp]\n");
	fprintf(stderr," -i illum        Choose illuminant for computation of CIE XYZ from spectral data & FWA:\n");
	fprintf(stderr,"                  A, C, D50 (def.), D50M2, D65, F5, F8, F10 or file.sp\n");
	fprintf(stderr," -o observ       Choose CIE Observer for spectral data:\n");
	fprintf(stderr,"                  1931_2 (def), 1964_10, S&B 1955_2, shaw, J&V 1978_2\n");
	fprintf(stderr," -I intent       r = relative colorimetric, a = absolute (default)\n");
	fprintf(stderr," data.ti3        Test data file\n");
	fprintf(stderr," iccprofile.icm  Profile to check against\n");
	exit(1);
	}

static void DE2RGB(double *out, double in);

/* Patch value type */
typedef struct {
	char sid[50];		/* sample id */
	char slo[50];		/* sample location, "" if not known */
	double p[MAX_CHAN];	/* Device value */
	double v[3];		/* CIE value */

	double pv[3];		/* Profile CIE value */
	double de;			/* Delta E to profile CIE value */

	double dp;			/* Delta p[] from target device value */
	double dv;			/* Delta E from CIE value */
} pval;

/* Histogram bin type */
typedef struct {
	int count;			/* Raw count */
	double val;			/* Normalized value */
	double min, max;	/* Bin range */
} hbin;

int main(int argc, char *argv[])
{
	int fa,nfa;				/* current argument we're looking at */
	int verb = 0;
	int cie94 = 0;
	int cie2k = 0;
	int dovrml = 0;
	int dominl = 0;
	int doaxes = 0;
	int dodecol = 0;
	char ti3name[MAXNAMEL+1] = { 0 };	/* Input cgats file base name */
	cgats *icg;				/* input cgats structure */
	char iccname[MAXNAMEL+1] = { 0 };	/* Input icc file base name */
	icmFile *rd_fp;
	icRenderingIntent intent = icAbsoluteColorimetric;
	icc *rd_icco;
	icmLuBase *luo;
	char out_name[MAXNAMEL+1], *xl;		/* VRML/X3D name */
	vrml *wrl = NULL;

	int fwacomp = 0;			/* FWA compensation */
	int isdisp = 0;				/* nz if this is a display device, 0 if output */
	int isdnormed = 0;      	/* Has display data been normalised to 100 ? */
	int spec = 0;				/* Use spectral data flag */
	icxIllumeType tillum = icxIT_none;	/* Target/simulated instrument illuminant */ 
	xspect cust_tillum, *tillump = NULL; /* Custom target/simulated illumination spectrum */
	icxIllumeType illum = icxIT_D50;	/* Spectral defaults */
	xspect cust_illum;			/* Custom illumination spectrum */
	icxObserverType observ = icxOT_CIE_1931_2;

	int sortbyde = 0;			/* Sort by delta E */

	int ddevv = 0;				/* Do device value sort */
	double devval[MAX_CHAN];	/* device value to sort on */
	int sortbypcs = 0;			/* Sort by PCS error to device target */

	int dohisto = 0;			/* Plot histogram of delta E's */
	double prune = 0.0;			/* If > 0.0, created a pruned .ti3 file */

	int npat;					/* Number of patches */
	pval *tpat;					/* Patch input values */
	pval **stpat;				/* Pointers to internal sorted tpat[] */
	int i, j, rv = 0;
	icColorSpaceSignature devspace = 0;	/* The device colorspace */
	int isAdditive = 0;			/* 0 if subtractive, 1 if additive colorspace */
	int isLab = 0;				/* 0 if input is XYZ, 1 if input is Lab */
	int devchan = 0;			/* Number of device chanels */

#if defined(__IBMC__)
	_control87(EM_UNDERFLOW, EM_UNDERFLOW);
	_control87(EM_OVERFLOW, EM_OVERFLOW);
#endif

	error_program = "profcheck";

	if (argc <= 1)
		usage();

	/* Process the arguments */
	for(fa = 1;fa < argc;fa++) {

		nfa = fa;					/* skip to nfa if next argument is used */
		if (argv[fa][0] == '-') {	/* Look for any flags */
			char *na = NULL;		/* next argument after flag, null if none */

			if (argv[fa][2] != '\000')
				na = &argv[fa][2];		/* next is directly after flag */
			else {
				if ((fa+1) < argc) {
					if (argv[fa+1][0] != '-') {
						nfa = fa + 1;
						na = argv[nfa];		/* next is seperate non-flag argument */
					}
				}
			}

			if (argv[fa][1] == '?')
				usage();

			/* Verbosity */
			else if (argv[fa][1] == 'v') {
				verb = 1;
				if (na != NULL && isdigit(na[0])) {
					verb = atoi(na);
				}
			}

			/* VRML/X3D */
			else if (argv[fa][1] == 'w')
				dovrml = 1;

			/* Minimum line length */
			else if (argv[fa][1] == 'm')
				dominl = 1;

			/* Axes */
			else if (argv[fa][1] == 'x')
				doaxes = 1;

			/* Delta E coloring */
			else if (argv[fa][1] == 'e')
				dodecol = 1;

			else if (argv[fa][1] == 'c') {
				cie94 = 1;
				cie2k = 0;
			}

			else if (argv[fa][1] == 'k') {
				cie94 = 0;
				cie2k = 1;
			}

			/* Plot histogram */
			else if (argv[fa][1] == 'h')
				dohisto = 1;

			/* Sort by delta E */
			else if (argv[fa][1] == 's')
				sortbyde = 1;

			/* Create a pruned .ti3 */
			else if (argv[fa][1] == 'P') {
				fa = nfa;
				if (na == NULL) usage();
				prune = atof(na);
				if (prune <= 0.0)
					usage();
			}

			/* Device sort value */
			else if (argv[fa][1] == 'd') {
				char *tp, buf[200];
				int ndv;
				fa = nfa;
				if (na == NULL) usage();

				ddevv = 1;
				strcpy(buf, na);
				
				/* Replace ',' with '\000' */
				for (ndv = 1,tp = buf; *tp != '\000'; tp++) {
					if (*tp == ',') {
						*tp = '\000';
						ndv++;
					}
				}
				if (ndv >= MAX_CHAN)
					ndv = MAX_CHAN;

				for (tp = buf, i = 0; i < ndv; i++, tp += strlen(tp) + 1) {
					devval[i] = atof(tp);
				}
			}

			else if (argv[fa][1] == 'p')
				sortbypcs = 1;

			/* FWA compensation */
			else if (argv[fa][1] == 'f') {
				fwacomp = 1;

				if (na != NULL) {	/* Argument is present - target/simulated instr. illum. */
					fa = nfa;
					if (strcmp(na, "A") == 0
					 || strcmp(na, "M0") == 0) {
						spec = 1;
						tillum = icxIT_A;
					} else if (strcmp(na, "C") == 0) {
						spec = 1;
						tillum = icxIT_C;
					} else if (strcmp(na, "D50") == 0
					        || strcmp(na, "M1") == 0) {
						spec = 1;
						tillum = icxIT_D50;
					} else if (strcmp(na, "D50M2") == 0
					        || strcmp(na, "M2") == 0) {
						spec = 1;
						tillum = icxIT_D50M2;
					} else if (strcmp(na, "D65") == 0) {
						spec = 1;
						tillum = icxIT_D65;
					} else if (strcmp(na, "F5") == 0) {
						spec = 1;
						tillum = icxIT_F5;
					} else if (strcmp(na, "F8") == 0) {
						spec = 1;
						tillum = icxIT_F8;
					} else if (strcmp(na, "F10") == 0) {
						spec = 1;
						tillum = icxIT_F10;
					} else {	/* Assume it's a filename */
						spec = 1;
						tillum = icxIT_custom;
						if (read_xspect(&cust_tillum, na) != 0)
							usage();
					}
				}
			}
			/* Spectral Illuminant type */
			else if (argv[fa][1] == 'i') {
				fa = nfa;
				if (na == NULL) usage();
				if (strcmp(na, "A") == 0) {
					spec = 1;
					illum = icxIT_A;
				} else if (strcmp(na, "C") == 0) {
					spec = 1;
					illum = icxIT_C;
				} else if (strcmp(na, "D50") == 0) {
					spec = 1;
					illum = icxIT_D50;
				} else if (strcmp(na, "D50M2") == 0) {
					spec = 1;
					illum = icxIT_D50M2;
				} else if (strcmp(na, "D65") == 0) {
					spec = 1;
					illum = icxIT_D65;
				} else if (strcmp(na, "F5") == 0) {
					spec = 1;
					illum = icxIT_F5;
				} else if (strcmp(na, "F8") == 0) {
					spec = 1;
					illum = icxIT_F8;
				} else if (strcmp(na, "F10") == 0) {
					spec = 1;
					illum = icxIT_F10;
				} else {	/* Assume it's a filename */
					spec = 1;
					illum = icxIT_custom;
					if (read_xspect(&cust_illum, na) != 0)
						usage();
				}
			}

			/* Spectral Observer type */
			else if (argv[fa][1] == 'o') {
				fa = nfa;
				if (na == NULL) usage();
				if (strcmp(na, "1931_2") == 0) {			/* Classic 2 degree */
					spec = 1;
					observ = icxOT_CIE_1931_2;
				} else if (strcmp(na, "1964_10") == 0) {	/* Classic 10 degree */
					spec = 1;
					observ = icxOT_CIE_1964_10;
				} else if (strcmp(na, "1955_2") == 0) {		/* Stiles and Burch 1955 2 degree */
					spec = 1;
					observ = icxOT_Stiles_Burch_2;
				} else if (strcmp(na, "1978_2") == 0) {		/* Judd and Voss 1978 2 degree */
					spec = 1;
					observ = icxOT_Judd_Voss_2;
				} else if (strcmp(na, "shaw") == 0) {		/* Shaw and Fairchilds 1997 2 degree */
					spec = 1;
					observ = icxOT_Shaw_Fairchild_2;
				} else
					usage();
			}

			/* Intent (only applies to ICC profile) */
			else if (argv[fa][1] == 'I') {
				fa = nfa;
				if (na == NULL) usage();
    			switch (na[0]) {
					case 'r':
						intent = icRelativeColorimetric;
						break;
					case 'a':
						intent = icAbsoluteColorimetric;
						break;
					default:
						usage();
				}
			}

			else 
				usage();
		} else
			break;
	}

	/* Get the file name arguments */
	if (fa >= argc || argv[fa][0] == '-') usage();
	strncpy(ti3name,argv[fa++],MAXNAMEL); ti3name[MAXNAMEL] = '\000';

	if (fa >= argc || argv[fa][0] == '-') usage();
	strncpy(iccname,argv[fa++],MAXNAMEL); iccname[MAXNAMEL] = '\000';

	strncpy(out_name,iccname,MAXNAMEL-4); out_name[MAXNAMEL-4] = '\000';
	if ((xl = strrchr(out_name, '.')) == NULL)	/* Figure where extention is */
		xl = out_name + strlen(out_name);
	xl[0] = '\000';			/* Remove extension */

	if (fwacomp && spec == 0)
		error ("FWA compensation only works when viewer and/or illuminant selected");

	/* Open and look at the .ti3 profile patches file */
	icg = new_cgats();			/* Create a CGATS structure */
	icg->add_other(icg, "CTI3"); 	/* our special input type is Calibration Target Information 3 */

	if (icg->read_name(icg, ti3name))
		error("CGATS file read error on '%s': %s",ti3name,icg->err);

	if (icg->ntables == 0 || icg->t[0].tt != tt_other || icg->t[0].oi != 0)
		error ("Input file '%s' isn't a CTI3 format file",ti3name);
	if (icg->ntables < 1)
		error ("Input file '%s' doesn't contain at least one table",ti3name);

	/* See if CIE is actually available - some sources of .TI3 don't provide it */
	if (!spec
	 && icg->find_field(icg, 0, "LAB_L") < 0
	 && icg->find_field(icg, 0, "XYZ_X") < 0) {

		if (icg->find_kword(icg, 0, "SPECTRAL_BANDS") < 0)
			error ("Neither CIE nor spectral data found in file '%s'",ti3name);

		/* Switch to using spectral information */
		if (verb)
			printf("No CIE data found, switching to spectral with standard observer & D50\n");
		spec = 1;
		illum = icxIT_D50;
		observ = icxOT_CIE_1931_2;
	}
	
	/* Figure out what sort of device it is */
	{
		int ti;

		if ((ti = icg->find_kword(icg, 0, "DEVICE_CLASS")) < 0)
			error ("Input file '%s' doesn't contain keyword DEVICE_CLASS",ti3name);

		if (strcmp(icg->t[0].kdata[ti],"DISPLAY") == 0) {
			isdisp = 1;
		}

		/* See if the CIE data has been normalised to Y = 100 */
		if ((ti = icg->find_kword(icg, 0, "NORMALIZED_TO_Y_100")) < 0
		 || strcmp(icg->t[0].kdata[ti],"NO") == 0) {
			isdnormed = 0;
		} else {
			isdnormed = 1;
		}
	}

	/* Figure out what sort of device colorspace it is */
	{
		int ti;

		if ((ti = icg->find_kword(icg, 0, "COLOR_REP")) < 0)
			error("Input file '%s' doesn't contain keyword COLOR_REPS",ti3name);

		if (strcmp(icg->t[0].kdata[ti],"CMYK_XYZ") == 0) {
			devspace = icSigCmykData;
			devchan = 4;
			isLab = 0;
			isAdditive = 0;
		} else if (strcmp(icg->t[0].kdata[ti],"CMYK_LAB") == 0) {
			devspace = icSigCmykData;
			devchan = 4;
			isLab = 1;
			isAdditive = 0;
		} else if (strcmp(icg->t[0].kdata[ti],"CMY_XYZ") == 0) {
			devspace = icSigCmyData;
			devchan = 3;
			isLab = 0;
			isAdditive = 0;
		} else if (strcmp(icg->t[0].kdata[ti],"CMY_LAB") == 0) {
			devspace = icSigCmyData;
			devchan = 3;
			isLab = 1;
			isAdditive = 0;
		} else if (strcmp(icg->t[0].kdata[ti],"RGB_XYZ") == 0) {
			devspace = icSigRgbData;
			devchan = 3;
			isLab = 0;
			isAdditive = 1;
		} else if (strcmp(icg->t[0].kdata[ti],"RGB_LAB") == 0) {
			devspace = icSigRgbData;
			devchan = 3;
			isLab = 1;
			isAdditive = 1;
		} else if (strcmp(icg->t[0].kdata[ti],"iRGB_XYZ") == 0) {
			devspace = icSigRgbData;
			devchan = 3;
			isLab = 0;
			isAdditive = 1;
		} else if (strcmp(icg->t[0].kdata[ti],"iRGB_LAB") == 0) {
			devspace = icSigRgbData;
			devchan = 3;
			isLab = 1;
			isAdditive = 1;
		/* Scanner .ti3 files: */
		} else if (strcmp(icg->t[0].kdata[ti],"XYZ_RGB") == 0) {
			devspace = icSigRgbData;
			devchan = 3;
			isLab = 0;
			isAdditive = 1;
		} else if (strcmp(icg->t[0].kdata[ti],"LAB_RGB") == 0) {
			devspace = icSigRgbData;
			devchan = 3;
			isLab = 1;
			isAdditive = 1;
#ifdef IMP_MONO
		} else if (strcmp(icg->t[0].kdata[ti],"K_XYZ") == 0) {
			devspace = icSigGrayData;
			devchan = 1;
			isLab = 0;
			isAdditive = 0;
		} else if (strcmp(icg->t[0].kdata[ti],"K_LAB") == 0) {
			devspace = icSigGrayData;
			devchan = 1;
			isLab = 1;
			isAdditive = 0;
		} else if (strcmp(icg->t[0].kdata[ti],"W_XYZ") == 0) {
			devspace = icSigGrayData;
			devchan = 1;
			isLab = 0;
			isAdditive = 1;
		} else if (strcmp(icg->t[0].kdata[ti],"W_LAB") == 0) {
			devspace = icSigGrayData;
			devchan = 1;
			isLab = 1;
			isAdditive = 1;
#endif /* IMP_MONO */

		} else 
			error("Device input file '%s' has unhandled color representation '%s'",
			                                                     ti3name, icg->t[0].kdata[ti]);
	}

	if ((npat = icg->t[0].nsets) <= 0)
		error("Input file '%s' has no sets of data",ti3name);

	if (verb) {
		fprintf(verbo,"No of test patches = %d\n",npat);
	}

	/* Allocate arrays to hold test patch input and output values */
	if ((tpat = (pval *)malloc(sizeof(pval) * npat)) == NULL)
		error("Malloc failed - tpat[]");

	if ((stpat = (pval **)malloc(sizeof(pval *) * npat)) == NULL)
		error("Malloc failed - stpat[]");

	for (i = 0; i < npat; i++)
		stpat[i] = &tpat[i];

	/* Read in the CGATs fields */
	{
		int sidx;					/* Sample ID index */
		int sloc;					/* Sample location index (if any) */
		int ti, ci, mi, yi, ki;
		int Xi, Yi, Zi;

		if ((sidx = icg->find_field(icg, 0, "SAMPLE_ID")) < 0)
			error("Input file '%s' doesn't contain field SAMPLE_ID",ti3name);
		if (icg->t[0].ftype[sidx] != nqcs_t)
			error("Input file '%s' field SAMPLE_ID is wrong type",ti3name);

		if ((sloc = icg->find_field(icg, 0, "SAMPLE_LOC")) >= 0) {
			if (icg->t[0].ftype[sloc] != cs_t)
				error("Input file '%s' field SAMPLE_LOC is wrong type",ti3name);
		}

		if (devspace == icSigGrayData) {
			if (isAdditive) {
				if ((ci = icg->find_field(icg, 0, "GRAY_W")) < 0)
					error("Input file doesn't contain field GRAY_W");
				if (icg->t[0].ftype[ci] != r_t)
					error("Field GRAY_W is wrong type - corrupted file ?");
			} else {
				if ((ci = icg->find_field(icg, 0, "GRAY_K")) < 0)
					error("Input file doesn't contain field GRAY_K");
				if (icg->t[0].ftype[ci] != r_t)
					error("Field GRAY_K is wrong type - corrupted file ?");
			}
			mi = yi = ki = ci;

		} else if (devspace == icSigRgbData) {
			if ((ci = icg->find_field(icg, 0, "RGB_R")) < 0)
				error("Input file '%s' doesn't contain field RGB_R",ti3name);
			if (icg->t[0].ftype[ci] != r_t)
				error("Input file '%s' field RGB_R is wrong type",ti3name);
			if ((mi = icg->find_field(icg, 0, "RGB_G")) < 0)
				error("Input file '%s' doesn't contain field RGB_G",ti3name);
			if (icg->t[0].ftype[mi] != r_t)
				error("Input file '%s' field RGB_G is wrong type",ti3name);
			if ((yi = icg->find_field(icg, 0, "RGB_B")) < 0)
				error("Input file '%s' doesn't contain field RGB_B",ti3name);
			if (icg->t[0].ftype[yi] != r_t)
				error("Input file '%s' field RGB_B is wrong type",ti3name);
			ki = yi;

		} else if (devspace == icSigCmyData) {

			if ((ci = icg->find_field(icg, 0, "CMY_C")) < 0)
				error("Input file '%s' doesn't contain field CMY_C",ti3name);
			if (icg->t[0].ftype[ci] != r_t)
				error("Input file '%s' field CMY_C is wrong type",ti3name);
			if ((mi = icg->find_field(icg, 0, "CMY_M")) < 0)
				error("Input file '%s' doesn't contain field CMY_M",ti3name);
			if (icg->t[0].ftype[mi] != r_t)
				error("Input file '%s' field CMY_M is wrong type",ti3name);
			if ((yi = icg->find_field(icg, 0, "CMY_Y")) < 0)
				error("Input file '%s' doesn't contain field CMY_Y",ti3name);
			ki = yi;
		} else {	/* Assume CMYK */

			if ((ci = icg->find_field(icg, 0, "CMYK_C")) < 0)
				error("Input file '%s' doesn't contain field CMYK_C",ti3name);
			if (icg->t[0].ftype[ci] != r_t)
				error("Input file '%s' field CMYK_C is wrong type",ti3name);
			if ((mi = icg->find_field(icg, 0, "CMYK_M")) < 0)
				error("Input file '%s' doesn't contain field CMYK_M",ti3name);
			if (icg->t[0].ftype[mi] != r_t)
				error("Input file '%s' field CMYK_M is wrong type",ti3name);
			if ((yi = icg->find_field(icg, 0, "CMYK_Y")) < 0)
				error("Input file '%s' doesn't contain field CMYK_Y",ti3name);
			if (icg->t[0].ftype[yi] != r_t)
				error("Input file '%s' field CMYK_Y is wrong type",ti3name);
			if ((ki = icg->find_field(icg, 0, "CMYK_K")) < 0)
				error("Input file '%s' doesn't contain field CMYK_K",ti3name);
			if (icg->t[0].ftype[ki] != r_t)
				error("Input file '%s' field CMYK_K is wrong type",ti3name);
		}

		if (spec == 0) { 		/* Using instrument tristimulous value */

			if (isLab) {		/* Expect Lab */
				if ((Xi = icg->find_field(icg, 0, "LAB_L")) < 0)
					error("Input file '%s' doesn't contain field LAB_L",ti3name);
				if (icg->t[0].ftype[Xi] != r_t)
					error("Input file '%s' field LAB_L is wrong type",ti3name);
				if ((Yi = icg->find_field(icg, 0, "LAB_A")) < 0)
					error("Input '%s' file doesn't contain field LAB_A",ti3name);
				if (icg->t[0].ftype[Yi] != r_t)
					error("Input file '%s' field LAB_A is wrong type",ti3name);
				if ((Zi = icg->find_field(icg, 0, "LAB_B")) < 0)
					error("Input file '%s' doesn't contain field LAB_B",ti3name);
				if (icg->t[0].ftype[Zi] != r_t)
					error("Input file '%s' field LAB_B is wrong type",ti3name);

			} else { 		/* Expect XYZ */
				if ((Xi = icg->find_field(icg, 0, "XYZ_X")) < 0)
					error("Input file '%s' doesn't contain field XYZ_X",ti3name);
				if (icg->t[0].ftype[Xi] != r_t)
					error("Input file '%s' field XYZ_X is wrong type",ti3name);
				if ((Yi = icg->find_field(icg, 0, "XYZ_Y")) < 0)
					error("Input file '%s' doesn't contain field XYZ_Y",ti3name);
				if (icg->t[0].ftype[Yi] != r_t)
					error("Input file '%s' field XYZ_Y is wrong type",ti3name);
				if ((Zi = icg->find_field(icg, 0, "XYZ_Z")) < 0)
					error("Input file '%s' doesn't contain field XYZ_Z",ti3name);
				if (icg->t[0].ftype[Zi] != r_t)
					error("Input file '%s' field XYZ_Z is wrong type",ti3name);
			}

			for (i = 0; i < npat; i++) {
				strcpy(tpat[i].sid, (char *)icg->t[0].fdata[i][sidx]);
				if (sloc >= 0) 
					strcpy(tpat[i].slo, (char *)icg->t[0].fdata[i][sloc]);
				else
					strcpy(tpat[i].slo, "");
				tpat[i].p[0] = *((double *)icg->t[0].fdata[i][ci]) / 100.0;
				tpat[i].p[1] = *((double *)icg->t[0].fdata[i][mi]) / 100.0;
				tpat[i].p[2] = *((double *)icg->t[0].fdata[i][yi]) / 100.0;
				tpat[i].p[3] = *((double *)icg->t[0].fdata[i][ki]) / 100.0;
				if (tpat[i].p[0] > 1.0
				 || tpat[i].p[1] > 1.0
				 || tpat[i].p[2] > 1.0
				 || tpat[i].p[3] > 1.0) {
					error("Input file '%s' device value field value exceeds 100.0 !",ti3name);
				}
				tpat[i].v[0] = *((double *)icg->t[0].fdata[i][Xi]);
				tpat[i].v[1] = *((double *)icg->t[0].fdata[i][Yi]);
				tpat[i].v[2] = *((double *)icg->t[0].fdata[i][Zi]);
				if (!isLab && (!isdisp || isdnormed != 0)) {
					tpat[i].v[0] /= 100.0;		/* Normalise XYZ to range 0.0 - 1.0 */
					tpat[i].v[1] /= 100.0;
					tpat[i].v[2] /= 100.0;
				}
				if (!isLab) { /* Convert test patch result XYZ to PCS (D50 Lab) */
					icmXYZ2Lab(&icmD50, tpat[i].v, tpat[i].v);
				}
			}

		} else { 		/* Using spectral data */
			int ii;
			xspect sp;
			char buf[100];
			int  spi[XSPECT_MAX_BANDS];	/* CGATS indexes for each wavelength */
			xsp2cie *sp2cie;	/* Spectral conversion object */

			if ((ii = icg->find_kword(icg, 0, "SPECTRAL_BANDS")) < 0)
				error ("Input file '%s' doesn't contain keyword SPECTRAL_BANDS",ti3name);
			sp.spec_n = atoi(icg->t[0].kdata[ii]);
			if ((ii = icg->find_kword(icg, 0, "SPECTRAL_START_NM")) < 0)
				error ("Input file '%s' doesn't contain keyword SPECTRAL_START_NM",ti3name);
			sp.spec_wl_short = atof(icg->t[0].kdata[ii]);
			if ((ii = icg->find_kword(icg, 0, "SPECTRAL_END_NM")) < 0)
				error ("Input file '%s; doesn't contain keyword SPECTRAL_END_NM",ti3name);
			sp.spec_wl_long = atof(icg->t[0].kdata[ii]);
			if (!isdisp || isdnormed != 0)
				sp.norm = 100.0;
			else
				sp.norm = 1.0;

			/* Find the fields for spectral values */
			for (j = 0; j < sp.spec_n; j++) {
				int nm;
		
				/* Compute nearest integer wavelength */
				nm = (int)(sp.spec_wl_short + ((double)j/(sp.spec_n-1.0))
				            * (sp.spec_wl_long - sp.spec_wl_short) + 0.5);
				
				sprintf(buf,"SPEC_%03d",nm);

				if ((spi[j] = icg->find_field(icg, 0, buf)) < 0)
					error("Input file '%s' doesn't contain field %s",ti3name,buf);
			}

			if (isdisp) {
				illum = icxIT_none;		/* Displays are assumed to be self luminous */
			}

			/* Create a spectral conversion object */
			if ((sp2cie = new_xsp2cie(illum, illum == icxIT_none ? NULL : &cust_illum,
			                          observ, NULL, icSigLabData, icxClamp)) == NULL)
				error("Creation of spectral conversion object failed");

			if (fwacomp) {
				double nw = 0.0;		/* Number of media white patches */
				xspect mwsp;			/* Medium spectrum */
				instType itype;			/* Spectral instrument type */
				xspect insp;			/* Instrument illuminant */

				mwsp = sp;		/* Struct copy */

	 			if ((ti = icg->find_kword(icg, 0, "TARGET_INSTRUMENT")) < 0)
					error ("Input file '%s' can't find target instrument needed for FWA compensation",ti3name);

				if ((itype = inst_enum(icg->t[0].kdata[ti])) == instUnknown)
					error ("Input file '%s' unrecognised target instrument '%s'",ti3name, icg->t[0].kdata[ti]);

				if (inst_illuminant(&insp, itype) != 0)
					error ("Instrument doesn't have an FWA illuminent");

				/* Find the media white spectral reflectance */
				for (j = 0; j < mwsp.spec_n; j++)
					mwsp.spec[j] = 0.0;

				/* Compute the mean of all the media white patches */
				for (i = 0; i < npat; i++) {
					int use = 0;
	
					if (devspace == icSigGrayData) {
						if (isAdditive) {
							if (*((double *)icg->t[0].fdata[i][ci]) > (100.0 - 0.1))
								use = 1;
						} else {
							if (*((double *)icg->t[0].fdata[i][ci]) < 0.1)
								use = 1;
						}
					} else if (devspace == icSigRgbData) {
						if (*((double *)icg->t[0].fdata[i][ci]) > (100.0 - 0.1)
						 && *((double *)icg->t[0].fdata[i][mi]) > (100.0 - 0.1)
						 && *((double *)icg->t[0].fdata[i][yi]) > (100.0 - 0.1))
							use = 1;
					} else if (devspace == icSigCmyData) {
						if (*((double *)icg->t[0].fdata[i][ci]) < 0.1
						 && *((double *)icg->t[0].fdata[i][mi]) < 0.1
						 && *((double *)icg->t[0].fdata[i][yi]) < 0.1)
							use = 1;
					} else {	/* Assume CMYK */

						if (*((double *)icg->t[0].fdata[i][ci]) < 0.1
						 && *((double *)icg->t[0].fdata[i][mi]) < 0.1
						 && *((double *)icg->t[0].fdata[i][yi]) < 0.1
						 && *((double *)icg->t[0].fdata[i][ki]) < 0.1) {
							use = 1;
						}
					}

					if (use) {
						/* Read the spectral values for this patch */
						for (j = 0; j < mwsp.spec_n; j++) {
							mwsp.spec[j] += *((double *)icg->t[0].fdata[i][spi[j]]);
						}
						nw++;
					}
				}
				if (nw == 0.0) {
					warning("Input file '%s' can't find a media white patch to init FWA",ti3name);

					/* Track the maximum reflectance for any band to determine white. */
					/* This might give bogus results if there is no white patch... */
					for (i = 0; i < npat; i++) {
						for (j = 0; j < mwsp.spec_n; j++) {
							double rv = *((double *)icg->t[0].fdata[i][spi[j]]);
							if (rv > mwsp.spec[j])
								mwsp.spec[j] = rv;
						}
					}
					nw++;
				}

				for (j = 0; j < mwsp.spec_n; j++)
					mwsp.spec[j] /= nw;	/* Compute average */

				/* If we are setting a specific simulated instrument illuminant */
				if (tillum != icxIT_none) {
					tillump = &cust_tillum;
					if (tillum != icxIT_custom) {
						if (standardIlluminant(tillump, tillum, 0.0)) {
							error("simulated inst. illum. not recognised");
						}
					}
				}

				if (sp2cie->set_fwa(sp2cie, &insp, tillump, &mwsp)) 
					error ("Set FWA on sp2cie failed");

				if (verb) {
					double FWAc;
					sp2cie->get_fwa_info(sp2cie, &FWAc);
					fprintf(verbo,"FWA content = %f\n",FWAc);
				}
				
			}

			for (i = 0; i < npat; i++) {
				strcpy(tpat[i].sid, (char *)icg->t[0].fdata[i][sidx]);
				if (sloc >= 0) 
					strcpy(tpat[i].slo, (char *)icg->t[0].fdata[i][sloc]);
				else
					strcpy(tpat[i].slo, "");
				tpat[i].p[0] = *((double *)icg->t[0].fdata[i][ci]) / 100.0;
				tpat[i].p[1] = *((double *)icg->t[0].fdata[i][mi]) / 100.0;
				tpat[i].p[2] = *((double *)icg->t[0].fdata[i][yi]) / 100.0;
				tpat[i].p[3] = *((double *)icg->t[0].fdata[i][ki]) / 100.0;
				if (tpat[i].p[0] > 1.0
				 || tpat[i].p[1] > 1.0
				 || tpat[i].p[2] > 1.0
				 || tpat[i].p[3] > 1.0) {
					error("Input file '%s' device value field value exceeds 100.0 !",ti3name);
				}

				/* Read the spectral values for this patch */
				for (j = 0; j < sp.spec_n; j++) {
					sp.spec[j] = *((double *)icg->t[0].fdata[i][spi[j]]);
				}

				/* Convert it to CIE space */
				sp2cie->convert(sp2cie, tpat[i].v, &sp);
			}

			sp2cie->del(sp2cie);		/* Done with this */
		}
		/* Normalize display values to Y = 1.0 if needed */
		/* (re-norm spec derived, since observer may be different) */
		if (isdisp && (isdnormed == 0 || spec != 0)) {
			double scale = -1e6;
			double bxyz[3];

			/* Locate max Y */
			for (i = 0; i < npat; i++) {
				icmLab2XYZ(&icmD50, bxyz,  tpat[i].v);
				if (bxyz[1] > scale)
					scale = bxyz[1];
			}
			
			scale = 1.0/scale;

			/* Scale max Y to 1.0 */
			for (i = 0; i < npat; i++) {
				icmLab2XYZ(&icmD50, tpat[i].v, tpat[i].v);
				tpat[i].v[0] *= scale;
				tpat[i].v[1] *= scale;
				tpat[i].v[2] *= scale;
				icmXYZ2Lab(&icmD50, tpat[i].v, tpat[i].v);
			}
		}

	}	/* End of reading in CGATs file */

	/* - - - - - - - - - - */
	/* Compute the delta E of each point against the profile value */

	/* Open up the file for reading */
	if ((rd_fp = new_icmFileStd_name(iccname,"r")) == NULL)
		error("Write: Can't open file '%s'",iccname);

	if ((rd_icco = new_icc()) == NULL)
		error("Read: Creation of ICC object failed");

	/* Read the header and tag list */
	if ((rv = rd_icco->read(rd_icco,rd_fp,0)) != 0)
		error("Read: %d, %s",rv,rd_icco->err);

	/* Get the Fwd table, absolute with Lab override */
	if ((luo = rd_icco->get_luobj(rd_icco, icmFwd, intent,
	                              icSigLabData, icmLuOrdNorm)) == NULL) {
		error("%d, %s",rd_icco->errc, rd_icco->err);
	}

	for (i = 0; i < npat; i++) {

		/* Lookup the patch value in the profile */
		if (luo->lookup(luo, tpat[i].pv, tpat[i].p) > 1)
			error("%d, %s",rd_icco->errc,rd_icco->err);

		if (cie2k)
			tpat[i].de = icmCIE2K(tpat[i].v, tpat[i].pv);
		else if (cie94)
			tpat[i].de = icmCIE94(tpat[i].v, tpat[i].pv);
		else
			tpat[i].de = icmLabDE(tpat[i].v, tpat[i].pv);
	}

	/* - - - - - - - - - - */
	/* Sort by delta E */
	if (sortbyde) {
		/* Sort the dE's */
#define HEAP_COMPARE(A,B) (A.de > B.de)
		HEAPSORT(pval, tpat, npat);
#undef HEAP_COMPARE
	}

	/* - - - - - - - - - - */
	/* Plot a dE histogram */
	if (dohisto) {
		double demax = -1e6, demin = 1e6;
		int maxbins = 50;		/* Maximum bins */
		int minbins = 20;		/* Target minimum bins (depends on distribution) */ 
		int mincount = 10;		/* Minimum number of points in a bin */
		double mbwidth;
		int nbins = 0;
		hbin *bins = NULL;
		double tval;
		double *x, *y;
		
		/* Figure out the range of dE's */
		for (i = 0; i < npat; i++) {
			double de = tpat[i].de;

			if (de > demax)
				demax = de;
			if (de < demin)
				demin = de;
		}

		if (demax < 1e-6)
			error("histogram: dE range is too small to plot");

		/* Bin width that gives maxbins */
		mbwidth = demax / maxbins;
		
		/* Reduce mincount if needed to get minbins */
		if (npat/minbins < mincount)
			mincount = npat/minbins;

		if ((bins = (hbin *)calloc(maxbins, sizeof(hbin))) == NULL)
			error("malloc of histogram bins failed");

		/* Sort the dE's */
#define HEAP_COMPARE(A,B) (A->de < B->de)
		HEAPSORT(pval *, stpat, npat);
#undef HEAP_COMPARE

		/* Create bins and add points */
		bins[0].min = 0.0;
		for (nbins = i = 0; i < npat && nbins < maxbins; i++) {
			double de = stpat[i]->de;

			/* Move on to next bin ? */
			if (bins[nbins].count >= mincount
			 && (de - bins[nbins].min) >= mbwidth) {
				if (i > 0)
					bins[nbins].max = 0.5 * (de + stpat[i-1]->de);
				else
					bins[nbins].max = de;
				nbins++;
				bins[nbins].min = bins[nbins-1].max; 
			} 
			bins[nbins].count++;
			bins[nbins].max = de;
		}
		if (bins[nbins].count != 0)
			nbins++;

		/* Compute value */
		tval = 0.0;
		for (i = 0; i < nbins; i++) {
			bins[i].val = bins[i].count/(bins[i].max - bins[i].min);
			tval += bins[i].val;
		}

		tval /= 100.0;		/* Make it % */
		for (i = 0; i < nbins; i++) {
			bins[i].val /= tval;
			if (verb) fprintf(verbo,"Bin %d, %f - %f, % 2.4f%%, count %d\n",
			             i,bins[i].min,bins[i].max,bins[i].val,bins[i].count);
		}

		/* Plot it */
		if ((x = (double *)calloc(nbins+1, sizeof(double))) == NULL)
			error("malloc of histogram x array");
		if ((y = (double *)calloc(nbins+1, sizeof(double))) == NULL)
			error("malloc of histogram y array");

		for (i = 0; i < nbins; i++) {
			x[i] = 0.5 * (bins[i].min + bins[i].max);
			y[i] = bins[i].val;
		}
		x[i] = demax;
		y[i] = 0.0;
		do_plot(x, y, NULL, NULL, nbins+1);

		free(y);
		free(x);
		free(bins);
	}

	/* - - - - - - - - - - */
	/* Create a pruned .ti3 file */
	if (prune > 0.0) {
		char *cp, outname[MAXNAMEL+31];
		cgats *ocg;
		cgats_set_elem *setel;		/* Array of set value elements */

		strcpy(outname, ti3name);
		if ((cp = strrchr(outname, '.')) == NULL)
			cp = outname + strlen(outname);
		sprintf(cp, "_p%.2f.ti3",prune);
		
		if (verb) fprintf(verbo,"Created pruned file '%s'\n",outname);

		/* Create the output files */
		if ((ocg = new_cgats()) == NULL)
			error("Failed to create cgats object");
	
		/* Duplicate the type of the file */
		if (icg->t[0].tt == cgats_X) {
			ocg->add_other(ocg, icg->cgats_type);
			ocg->add_table(ocg, tt_other, 0);
		} else if (icg->t[0].tt == tt_other) {
			ocg->add_other(ocg, icg->others[icg->t[0].oi]);
			ocg->add_table(ocg, tt_other, 0);
		} else {
			ocg->add_table(ocg, icg->t[0].tt, 0);
		}
	
		/* Duplicate all the keywords */
		for (i = 0; i < icg->t[0].nkwords; i++) {
			ocg->add_kword(ocg, 0, icg->t[0].ksym[i], icg->t[0].kdata[i], NULL);
		}
	
		/* Duplicate all of the fields */
		for (i = 0; i < icg->t[0].nfields; i++) {
			ocg->add_field(ocg, 0, icg->t[0].fsym[i], icg->t[0].ftype[i]);
		}
	
		if ((setel = (cgats_set_elem *)malloc(
		     sizeof(cgats_set_elem) * icg->t[0].nfields)) == NULL)
			error("Malloc failed!");
	
		/* Copy them approproately */
		for (i = 0; i < icg->t[0].nsets; i++) {

			if (tpat[i].de <= prune) {
				icg->get_setarr(icg, 0, i, setel);
				ocg->add_setarr(ocg, 0, setel);
			}
		}
	
		if (verb) {
			double acc;

			acc = (double)ocg->t[0].nsets/(double)icg->t[0].nsets * 100.0;
			fprintf(verbo,"%.2f%% accepted, %.3f%% rejected\n",acc, 100.0-acc);
		}

		/* Write out the file */
		if (ocg->write_name(ocg, outname))
			error("CGATS file '%s' write error : %s",outname,ocg->err);
	}

	/* - - - - - - - - - - */
	/* Display various results */
	{
		double merr = 0.0;		/* Max */
		double aerr = 0.0;		/* Avg */
		double rerr = 0.0;		/* RMS */
		double nsamps = 0.0;
		int inn, outn;			/* Chanells for input and output spaces */

		if (dovrml) {
			wrl = new_vrml(out_name, doaxes, vrml_lab);
			wrl->start_line_set(wrl, 0);
		}

		/* Get details of conversion (Arguments may be NULL if info not needed) */
		luo->spaces(luo, NULL, &inn, NULL, &outn, NULL, NULL, NULL, NULL, NULL);

		for (i = 0; i < npat; i++) {
			double de, *out;

			de = tpat[i].de;
			out = tpat[i].pv;

			if (verb > 1) {
#ifdef HACK		// Print XYZ
#pragma message("!!!!!!!!!!!!!!!!!! profile/profcheck.c HACK enabled !!!!!!!!!!!!!!!")
				double outxyz[3], vxyz[3];
				icmLab2XYZ(&icmD50, outxyz, out);
				icmLab2XYZ(&icmD50, vxyz, tpat[i].v);

				printf("[%f] %s%s%s: %s -> %f %f %f should be %f %f %f\n",
				       de,
				       tpat[i].sid,
				       tpat[i].slo[0] != '\000' ? " @ " : "",
				       tpat[i].slo,
				       icmPdv(devchan, tpat[i].p),
				       outxyz[0],outxyz[1],outxyz[2],
				       vxyz[0],vxyz[1],vxyz[2]);
#else
				printf("[%f] %s%s%s: %s -> %f %f %f should be %f %f %f\n",
				       de,
				       tpat[i].sid,
				       tpat[i].slo[0] != '\000' ? " @ " : "",
				       tpat[i].slo,
				       icmPdv(devchan, tpat[i].p),
				       out[0],out[1],out[2],
				       tpat[i].v[0],tpat[i].v[1],tpat[i].v[2]);
#endif
			}
			if (dovrml) {
				int ix[2];
				double p1[3], p2[3];

				/* Add the vertexes */
				if (dominl && de < 0.5) {		/* Make a minimum length */
					double cent[3], vec[3], vlen;

					/* Compute center */
					icmAdd3(cent, tpat[i].v, out);
					icmScale3(cent, cent, 0.5);
					if ((vlen = de) < 1e-6) {
						vec[0] = 0.25; vec[1] = 0.0; vec[2] = 0.0;
					} else {
						icmSub3(vec, tpat[i].v, out);
						icmScale3(vec, vec, 0.25/vlen);
					}
					icmSub3(p1, cent, vec);
					icmAdd3(p2, cent, vec);
				} else {
					icmCpy3(p1, tpat[i].v);
					icmCpy3(p2, out);
				}
				ix[0] = wrl->add_vertex(wrl, 0, p1);
				ix[1] = wrl->add_vertex(wrl, 0, p2);

				/* Add the line */
				if (dodecol) {		/* Lines with color determined by length */
					double rgb[3];
					DE2RGB(rgb, icmNorm33(p1, p2));
					wrl->add_col_line(wrl, 0, ix, rgb);

				} else {	/* Natural color */
					wrl->add_line(wrl, 0, ix);
				}
			}

			/* Stats */
			aerr += de;
			rerr += de * de;

			nsamps++;
			if (de > merr)
				merr = de;

		}
		if (dovrml) {
			wrl->make_lines_vc(wrl, 0, 0.0);
			wrl->del(wrl);
		}
		printf("Profile check complete, errors%s: max. = %f, avg. = %f, RMS = %f\n",
            cie2k ? "(CIEDE2000)" : cie94 ? " (CIE94)" : "", merr, aerr/nsamps, sqrt(rerr/nsamps));

		/* ------------------------------- */
		/* If we want sort by target value */
		if (ddevv) {
			double cieval[3];

			/* Lookup the CIE value of the target */
			if (luo->lookup(luo, cieval, devval) > 1)
				error("%d, %s",rd_icco->errc,rd_icco->err);

			/* Compute deltas to target value. */
			for (i = 0; i < npat; i++) {
				if (cie2k)
					tpat[i].dv = icmCIE2K(tpat[i].v, cieval);
				else if (cie94)
					tpat[i].dv = icmCIE94(tpat[i].v, cieval);
				else
					tpat[i].dv = icmLabDE(tpat[i].v, cieval);

				tpat[i].dp = 0.0;
				for (j = 0; j < inn; j++) {
					double tt;
					tt = tpat[i].p[j] - devval[j];
					tpat[i].dp += tt * tt;
				}
				tpat[i].dp = sqrt(tpat[i].dp);
			}

			if (sortbypcs) {
				/* Sort by pcs delta */
#define HEAP_COMPARE(A,B) (A->dv < B->dv)
				HEAPSORT(pval *, stpat, npat);
#undef HEAP_COMPARE
			} else {
				/* Sort by device delta */
#define HEAP_COMPARE(A,B) (A->dp < B->dp)
				HEAPSORT(pval *, stpat, npat);
#undef HEAP_COMPARE
			}

			printf("Target point:\n");
			if (devspace == icSigCmykData) {
				printf("%f %f %f %f -> %f %f %f\n",
				       devval[0],devval[1],devval[2],devval[3],
				       cieval[0],cieval[1],cieval[2]);
			} else {	/* Assume RGB/CMY */
				printf("%f %f %f -> %f %f %f\n",
				       devval[0],devval[1],devval[2],
				       cieval[0],cieval[1],cieval[2]);
			}
			printf("\n");
	
			for (i = 0; i < npat; i++) {
				if (devspace == icSigCmykData) {
					printf("%s: %f %f %f %f [%f] -> %f %f %f [%f]\n",
					       stpat[i]->sid,
					       stpat[i]->p[0],stpat[i]->p[1],stpat[i]->p[2],stpat[i]->p[3],
					       stpat[i]->dp,
					       stpat[i]->v[0],stpat[i]->v[1],stpat[i]->v[2],
					       stpat[i]->dv);
				} else {	/* Assume RGB/CMY */
					printf("%s: %f %f %f [%f] -> %f %f %f [%f]\n",
					       stpat[i]->sid,
					       stpat[i]->p[0],stpat[i]->p[1],stpat[i]->p[2],
					       stpat[i]->dp,
					       stpat[i]->v[0],stpat[i]->v[1],stpat[i]->v[2],
					       stpat[i]->dv);
				}
			}
		}

		/* Done with lookup object */
		luo->del(luo);

		/* Close the file */
		rd_icco->del(rd_icco);
		rd_fp->del(rd_fp);
	}

	icg->del(icg);		/* Clean up */

	return 0;
}


/* ------------------------------------------------ */

/* Convert a delta E value into a signal color: */
static void
DE2RGB(double *out, double in) {
	struct {
		double de;
		double r, g, b;
	} range[6] = {
		{ 10.0, 1, 1, 0 },		/* yellow */
		{ 4.0,  1, 0, 0 },		/* red */
		{ 2.0, 1, 0, 1 },		/* magenta */
		{ 1.0, 0, 0, 1 },		/* blue */
		{ 0.5, 0, 1, 1 },		/* cyan */
		{ 0.0, 0, 1, 0 }		/* green */
	};
	int i;
	double bl;

	/* Locate the range we're in */
	if (in > range[0].de) {
		out[0] = range[0].r;
		out[1] = range[0].g;
		out[2] = range[0].b;
	} else {
		for (i = 0; i < 5; i++) {
			if (in <= range[i].de && in >= range[i+1].de)
				break;
		}
		bl = (in - range[i+1].de)/(range[i].de - range[i+1].de);
		out[0] = bl * range[i].r + (1.0 - bl) * range[i+1].r;
		out[1] = bl * range[i].g + (1.0 - bl) * range[i+1].g;
		out[2] = bl * range[i].b + (1.0 - bl) * range[i+1].b;
	}
}



