
/* 
 * Argyll Color Management System
 * Fake print target chart reader - use ICC or MPP profile rather than instrument
 *
 * Author: Graeme W. Gill
 * Date:   17/2/2002
 *
 * Copyright 2002 - 2013 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.
 */

/*
 * TTBD: 
	
	This utility should really be merged with cctiff ?
		ie. Add cgats i/o to cctiff
		Add other processing steps such as TV, BT.1886, random to cctiff. 

	If a display profile has the icSigLuminanceTag, the value
	should be used to set the .ti3 LUMINANCE_XYZ_CDM2 value,
	and set NORMALIZED_TO_Y_100 to YES.

 */


#undef DEBUG

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <sys/types.h>
#include <time.h>
#include <string.h>
#include "copyright.h"
#include "aconfig.h"
#include "numlib.h"
#include "cgats.h"
#include "conv.h"
#include "xicc.h"
#include "bt1886.h"
#include "icc.h"
#include "ui.h"

void usage(char *diag, ...) {
	fprintf(stderr,"Fake test chart reader - lookup values in ICC/MPP profile, Version %s\n",
	               ARGYLL_VERSION_STR);
	fprintf(stderr,"Author: Graeme W. Gill, licensed under the AGPL Version 3\n");
	if (diag != NULL) {
		va_list args;
		fprintf(stderr,"  Diagnostic: ");
		va_start(args, diag);
		vfprintf(stderr, diag, args);
		va_end(args);
		fprintf(stderr,"\n");
	}
	/* Keep options in order of processing */
	fprintf(stderr,"usage: fakeread [-options] profile.[%s|mpp|ti3] outfile\n",ICC_FILE_EXT_ND);
	fprintf(stderr," -v [n]            Verbose mode [level]\n");
	fprintf(stderr," -e flag           Video encode device input to sepration as:\n");
	fprintf(stderr,"     n              normal 0..1 full range RGB levels (default)\n");
	fprintf(stderr,"     t              (16-235)/255 \"TV\" RGB levels\n");
	fprintf(stderr,"     6              Rec601 YCbCr SD (16-235,240)/255 \"TV\" levels\n");
	fprintf(stderr,"     7              Rec709 1125/60Hz YCbCr HD (16-235,240)/255 \"TV\" levels\n");
	fprintf(stderr,"     5              Rec709 1250/50Hz YCbCr HD (16-235,240)/255 \"TV\" levels\n");
	fprintf(stderr,"     2              Rec2020 YCbCr UHD (16-235,240)/255 \"TV\" levels\n");
	fprintf(stderr,"     C              Rec2020 Constant Luminance YCbCr UHD (16-235,240)/255 \"TV\" levels\n");
	fprintf(stderr," -p separation.%s Use device link separation profile on input\n",ICC_FILE_EXT_ND);
	fprintf(stderr," -E flag           Video decode separation device output. See -e above\n");
	fprintf(stderr," -Z nbits          Quantize test values to fit in nbits\n");
	fprintf(stderr," -k file.cal       Apply calibration (include in .ti3 output)\n");
	fprintf(stderr," -i file.cal       Include calibration in .ti3 output, but don't apply it\n");
	fprintf(stderr," -K file.cal       Apply inverse calibration\n");

	fprintf(stderr," -r level          Add average random deviation of <level>%% to device values (after sep. & cal.)\n");
	fprintf(stderr," -0 pow            Apply power to device chanel 0-9\n");
	fprintf(stderr," -B display.%s         Use BT.1886 source EOTF with technical gamma 2.4\n",ICC_FILE_EXT_ND);
	fprintf(stderr," -b g.g:display.%s     Use BT.1886-like source EOTF with effective gamma g.g\n",ICC_FILE_EXT_ND);
	fprintf(stderr," -b p.p:g.g:display.%s Use effective gamma g.g source EOTF with p.p prop. output black point offset\n",ICC_FILE_EXT_ND);
	fprintf(stderr," -g g.g:display.%s     Use effective gamma g.g source EOTF with all output black point offset\n",ICC_FILE_EXT_ND);
	fprintf(stderr," -I intent         r = relative colorimetric, a = absolute (default)\n");
	fprintf(stderr," -A L,a,b          Scale black point to target Lab value\n");
	fprintf(stderr," -l                Output Lab rather than XYZ\n");
	fprintf(stderr," -s                Lookup MPP spectral values\n");
	fprintf(stderr," -R level          Add average random deviation of <level>%% to output PCS values\n");
	fprintf(stderr," -u                Make random deviations have uniform distributions rather than normal\n");
	fprintf(stderr," -S seed           Set random seed\n");
	fprintf(stderr," -U                Reverse convert PCS to device, output_r.ti3\n");
	fprintf(stderr," -2                Input .ti2 rather than .ti1\n");
	fprintf(stderr," profile.[%s|mpp|ti3] ICC, MPP profile or TI3 to use\n",ICC_FILE_EXT_ND);
	fprintf(stderr," outfile           Base name for input[ti1]/output[ti3] file\n");
	exit(1);
	}

int main(int argc, char *argv[])
{
	int j;
	int fa, nfa, mfa;	/* current argument we're looking at */
	int verb = 0;		/* Verbose flag */
	int revlookup = 0;	/* Do PCS to device space conversion */
	int doti2 = 0;		/* Read .ti2 instead of .ti1 */
	int dosep = 0;		/* Use separation before profile */
	int bt1886 = 0;		/* 1 to apply BT.1886 black point & effective gamma to input */
						/* 2 to apply BT.1886 black point & technical gamma to input */
	double outoprop = 0.0;  /* Proportion of black output offset, 0.0 .. 1.0. 0.0 == BT.1886 */
	double egamma = 2.2;	/* effective BT.1886 style gamma to ain for */
	double tgamma = 2.4;	/* technical BT.1886 style gamma to ain for */
	bt1886_info bt;		/* BT.1886 adjustment info */
	int islab = 0;		/* Input has Lab rather than XYZ */
	int dolab = 0;		/* Output Lab rather than XYZ */
	int gfudge = 0;		/* Do grey fudge, 1 = W->RGB, 2 = K->xxxK */
	double chpow[10] = { 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0 };
	double rdlevel = 0.0;	/* Random device average deviation level (0.0 - 1.0) */
	double rplevel = 0.0;	/* Random PCS average deviation level (0.0 - 1.0) */
	int unidist = 0;		/* Use uniform distribution of errors */
	unsigned int seed = time(NULL);	/* Random seed value */
	double tbp[3] = { -1.0, 0.0, 0.0 };	/* Target black point */
	int applycal = 0;		/* 1 to apply calibration, 2 to apply inverse calibration */
	static char sepname[MAXNAMEL+1] = { 0 };	/* ICC separation profile */
	static char calname[MAXNAMEL+1] = { 0 };	/* device calibration */
	static char profname[MAXNAMEL+1] = { 0 };	/* ICC or MPP Profile name */
	static char inname[MAXNAMEL+1] = { 0 };	/* Input cgats file base name */
	static char outname[MAXNAMEL+3] = { 0 };	/* Output cgats file base name */
	static char odispname[MAXNAMEL+1] = { 0 };	/* BT.1886 display profile name */
	cgats *icg;			/* input cgats structure */
	cgats *ocg;			/* output cgats structure */
	int nmask = 0;		/* Test chart device colorant mask */
	int nchan = 0;		/* Test chart number of device chanels */
	int npat;			/* Number of patches */
	int si;				/* Sample id index */
	int ti;				/* Temp index */
	int fi;				/* Colorspace index */
	int inti3 = 0;		/* Input is a .ti3 format rather than .ti1 */

	/* TV encode/decode of separation/calibration device link */
	int in_tvenc = 0;		/* 1 to use RGB Video Level encoding, 2 = Rec601, etc. */
	int out_tvenc = 0;		/* 1 to use RGB Video Level encoding, 2 = Rec601, etc. */
	int qbits = 0;			/* Quantization bits, 0 = not set */

	/* ICC separation/calibration device link profile  */
	icmFile *sep_fp = NULL;		/* Color profile file */
	icmErr err = { 0, { '\000'} };
	icc *sep_icco = NULL;		/* Profile object */
	icmLuSpace *sep_luo = NULL;	/* Conversion object */
	icColorSpaceSignature sep_ins, sep_outs;	/* Type of input and output spaces */
	int sep_inn;				/* Number of input channels to separation */
	inkmask sep_nmask = 0;		/* Colorant mask for separation input */
	double wp[3], bp[3];		/* ICC profile Lab white and black points */
	double bpt[3][3];			/* Black point transform matrix (Lab->Lab) */

	/* Calibration */
	xcal *cal = NULL;			/* calibration */ 

	/* ICC profile based */
	icRenderingIntent intent = icAbsoluteColorimetric;
	icmFile *icc_fp = NULL;	/* Color profile file */
	icc *icc_icco = NULL;	/* Profile object */
	icmLuSpace *icc_luo = NULL;	/* Conversion object */
	icmLuAlgType alg;		/* Algorithm */

	/* MPP profile based */
	mpp *mlu = NULL;	/* Conversion object */
	instType itype = instUnknown;	/* Type of instrument used */
	int dospec = 0;		/* Do spectral MPP lookup */
	int spec_n = 0;		/* Number of spectral bands */
	double spec_wl_short;/* First reading wavelength in nm (shortest) */
	double spec_wl_long; /* Last reading wavelength in nm (longest) */

	/* TI3 based fake read */
	cgats *ti3 = NULL;			/* input cgats structure */
	int ti3_npat = 0;			/* Number of patches in reference file */
	int ti3_chix[ICX_MXINKS];	/* Device chanel indexes */
	int ti3_pcsix[3];			/* PCS chanel indexes */
	int ti3_spi[XSPECT_MAX_BANDS];	/* CGATS indexes for each wavelength */
	int ti3_isLab = 0;			/* Flag indicating PCS for TI3 file */

	int rv = 0;
	int inn, outn;		/* Number of channels for conversion input, output */
	icColorSpaceSignature ins, outs;	/* Type of conversion input and output spaces */
	inkmask cnv_nmask = 0;	/* Conversion input nmask */ 
	time_t clk = time(0);
	struct tm *tsp = localtime(&clk);
	char *atm = asctime(tsp); /* Ascii time */
	char *xyzfname[3] = { "XYZ_X", "XYZ_Y", "XYZ_Z" };
	char *labfname[3] = { "LAB_L", "LAB_A", "LAB_B" };


	error_program = "Fakeread";
	if (argc < 3)
		usage("Too few arguments");

	/* Process the arguments */
	mfa = 2;        /* Minimum final 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+mfa) < argc)
					{
					if (argv[fa+1][0] != '-')
						{
						nfa = fa + 1;
						na = argv[nfa];		/* next is seperate non-flag argument */
						}
					}
				}

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

			/* Verbose */
			else if (argv[fa][1] == 'v') {
				verb = 1;

				if (na != NULL && na[0] >= '0' && na[0] <= '9') {
					verb = atoi(na);
					fa = nfa;
				}
			}

			/* Spectral MPP lookup */
			else if (argv[fa][1] == 's')
				dospec = 1;

			/* Video RGB encoding */
			else if (argv[fa][1] == 'e'
			      || argv[fa][1] == 'E') {
				int enc;
				if (na == NULL) usage("Video encodong flag (-e/E) needs an argument");
    			switch (na[0]) {
					case 'n':				/* Normal */
						enc = 0;
						break;
					case 't':				/* TV 16 .. 235 */
						enc = 1;
						break;
					case '6':				/* Rec601 YCbCr */
						enc = 2;
						break;
					case '7':				/* Rec709 1150/60/2:1 YCbCr */
						enc = 3;
						break;
					case '5':				/* Rec709 1250/50/2:1 YCbCr (HD) */
						enc = 4;
						break;
					case '2':				/* Rec2020 Non-constant Luminance YCbCr (UHD) */
						enc = 5;
						break;
					case 'C':				/* Rec2020 Constant Luminance YCbCr (UHD) */
						enc = 6;
						break;
					default:
						usage("Video encoding (-E) argument not recognised");
				}
				if (argv[fa][1] == 'e')
					in_tvenc = enc;
				else {
					out_tvenc = enc;
					if (qbits == 0)
						qbits = 8;
				}
				fa = nfa;
			}

			/* Specify quantization bits */
			else if (argv[fa][1] == 'Z') {
				fa = nfa;
				if (na == NULL) usage("Expected argument to -Z");
				qbits = atoi(na);
				if (qbits < 1 || qbits > 32)
					usage("Argument to -Q must be between 1 and 32");
			}

			/* Separation */
			else if (argv[fa][1] == 'p') {
				dosep = 1;

				fa = nfa;
				if (na == NULL) usage("Expected an argument to -p");
				strncpy(sepname,na,MAXNAMEL); sepname[MAXNAMEL] = '\000';
			}

			/* Gamma curve modifier */
			else if (argv[fa][1] == 'b'
			      || argv[fa][1] == 'B'
			      || argv[fa][1] == 'g'
			      || argv[fa][1] == 'G') {
				char *cp;

				if (na == NULL) usage("Gamma curve flag (-%c) needs an argument",argv[fa][1]);
				
				bt1886 = 1;			/* Effective */
				if (argv[fa][1] == 'B' || argv[fa][1] == 'G')
					bt1886 = 2;		/* Technical */

				if (argv[fa][1] == 'g' || argv[fa][1] == 'G')
					outoprop = 1.0;

				/* Grab the filename */
				if ((cp = strrchr(na, ':')) != NULL) {
					double gamma, opr;

					strncpy(odispname,cp+1,MAXNAMEL); odispname[MAXNAMEL] = '\000';
					*cp = '\000';

					if (sscanf(na,"%lf:%lf",&opr, &gamma) == 2) {
						outoprop = opr;
						if (bt1886 == 1)
							egamma = gamma;
						else
							tgamma = gamma;

					} else if (sscanf(na,"%lf",&gamma) == 1) {
						if (bt1886 == 1)
							egamma = gamma;
						else
							tgamma = gamma;
					} else {
						usage("Gamma curve (-%c) arguments not recognised",argv[fa][1]);
					}

				} else {	/* No outoprop or gamma, just filanem */
					strncpy(odispname,na,MAXNAMEL); odispname[MAXNAMEL] = '\000';
				}
				fa = nfa;	/* Used argument */
			}

			/* Lab */
			else if (argv[fa][1] == 'l')
				dolab = 1;

			/* Uniform distrivuted errors */
			else if (argv[fa][1] == 'u')
				unidist = 1;

			/* Random seed value */
			else if (argv[fa][1] == 'S') {
				fa = nfa;
				if (na == NULL) usage("Expect argument to -S");
				if (sscanf(na, " %u ",&seed) != 1)
					usage("Couldn't parse argument to -S");
			}

			/* calibration to device values */
			else if (argv[fa][1] == 'k'
			     ||  argv[fa][1] == 'K'
			     ||  argv[fa][1] == 'i') {
				if (argv[fa][1] == 'K')
					applycal = 2;
				else if (argv[fa][1] == 'k')
					applycal = 1;
				else
					applycal = 0;
				fa = nfa;
				if (na == NULL) usage("Expected an argument to -%c",argv[fa][1]);
				strncpy(calname,na,MAXNAMEL); calname[MAXNAMEL] = '\000';
			}

			/* Extra flags */
			else if (argv[fa][1] == 'Y') {
				fa = nfa;
				if (na == NULL)
					usage("Parameter expected after -Y");
				{
					usage("-Y parameter '%c' not recognised",na[0]);
				}
			}

			/* Random addition to device levels */
			else if (argv[fa][1] == 'r') {
				fa = nfa;
				if (na == NULL) usage("Expect argument to -r");
				rdlevel = atof(na) * 0.01;
			}

			/* Power applied to device channels */
			else if (argv[fa][1] >= '0' && argv[fa][1] <= '9') {
				int ch;
				if (na == NULL) {
					if (argv[fa][1] == '2')
						doti2 = 1;		/* Read .ti2 instead of .ti1 */
					else
						usage("Expect power argument to -%c",argv[fa][1]);
				} else {
					ch = argv[fa][1]-'0';
					if (ch < 0 || ch > 9)
						usage("Channel no. %d is out of range\n",ch);
					chpow[ch] = atof(na);
					fa = nfa;
				}
			}

			/* Random addition to PCS levels */
			else if (argv[fa][1] == 'R') {
				fa = nfa;
				if (na == NULL) usage("Expect argument to -R");
				rplevel = atof(na) * 0.01;
			}

			/* Black point scale */
			else if (argv[fa][1] == 'A') {
				if (na == NULL) usage("Expect argument to -A");
				fa = nfa;
				if (sscanf(na, " %lf , %lf , %lf ",&tbp[0], &tbp[1], &tbp[2]) != 3)
					usage("Couldn't parse argument to -b");
				if (tbp[0] < 0.0 || tbp[0] > 100.0) usage("-b L* value out of range");
				if (tbp[1] < -128.0 || tbp[1] > 128.0) usage("-b a* value out of range");
				if (tbp[2] < -128.0 || tbp[2] > 128.0) usage("-b b* value out of range");
			}

			/* Intent (only applies to ICC profile) */
			else if (argv[fa][1] == 'I') {
				fa = nfa;
				if (na == NULL) usage("Expect argument to -I");
    			switch (na[0]) {
					case 'r':
						intent = icRelativeColorimetric;
						break;
					case 'a':
						intent = icAbsoluteColorimetric;
						break;
					default:
						usage("Unexpected argument value '%c' to -I optyion",na[0]);
				}
			}

			/* Reverse lookup */
			else if (argv[fa][1] == 'U') {
				revlookup = 1;
			}

			else
				usage("Unrecognised flag");
		}
		else
			break;
	}

	if (fa >= argc || argv[fa][0] == '-') usage("Missing profile filename argument");
	strncpy(profname,argv[fa++],MAXNAMEL); profname[MAXNAMEL] = '\000';

	if (fa >= argc || argv[fa][0] == '-') usage("Missing basename argument");
	strncpy(inname,argv[fa],MAXNAMEL-4); inname[MAXNAMEL-4] = '\000';
	if (revlookup)
		strcat(inname,".ti3");
	else {
		if (doti2)
			strcat(inname,".ti2");
		else
			strcat(inname,".ti1");
	}
	strncpy(outname,argv[fa],MAXNAMEL-4); outname[MAXNAMEL-4] = '\000';
	if (revlookup)
		strcat(outname,"_r");
	strcat(outname,".ti3");


	rand32(seed);		/* Init seed */

	if (revlookup && (
	    dosep
	 || calname[0] != '\000'
	 || dospec
	 || tbp[0] >= 0.0
	 || bt1886))
		error("Can't do separation, apply calibration, do spectral, black point scale, bt.1886, with reverse lookup");

	/* Deal with input CGATS files */
	icg = new_cgats();			/* Create a CGATS structure */
	if (doti2)
		icg->add_other(icg, "CTI2"); 	/* our special input type is Calibration Target Information 1 */
	else
		icg->add_other(icg, "CTI1"); 	/* our special input type is Calibration Target Information 1 */
	icg->add_other(icg, "CTI3"); 	/* also accept renamed .ti3 file */
	icg->add_other(icg, "CAL");

	if (icg->read_name(icg, inname))
		error("CGATS file read error : %s",icg->e.m);

	if (icg->ntables == 0 || icg->t[0].tt != tt_other || (icg->t[0].oi != 0 && icg->t[0].oi != 1))
		error ("Input file isn't a CTI1 format file");
	if (icg->t[0].oi == 1)
		inti3 = 1;			/* It's a renamed .ti3 file */
	if (icg->ntables != 1 && icg->ntables != 2 && icg->ntables != 3)
		error ("Input file doesn't contain one, two or three tables");

	if ((npat = icg->t[0].nsets) <= 0)
		error ("No sets of data");

	/* Figure out the color space of the .ti1 */
	if ((fi = icg->find_kword(icg, 0, "COLOR_REP")) < 0)
		error ("Input file doesn't contain keyword COLOR_REP");

	if (inti3) {
		char *rbuf, *outc;

		if ((rbuf = strdup(icg->t[0].kdata[fi])) == NULL)
			error("Malloc failed");
	
		/* Split COLOR_REP into device and PCS space */
		if ((outc = strchr(rbuf, '_')) == NULL)
			error("Input file '%s' COLOR_REP '%s' invalid", inname, icg->t[0].kdata[fi]);
		*outc++ = '\000';
	
		if ((nmask = icx_char2inkmask(rbuf)) == 0) {
			error ("Input file '%s' keyword COLOR_REP has unknown device value '%s'",inname,rbuf);
		}

		if (strcmp(outc,"LAB") == 0)
			islab = 1;
		else if (strcmp(outc,"XYZ") == 0)
			islab = 0;
		else
			error("Input .ti3 file PCS is neither LAB nor XYZ");
		dolab = islab;

		free(rbuf);
	} else {
		if ((nmask = icx_char2inkmask(icg->t[0].kdata[fi])) == 0)
			error ("Input file '%s' keyword COLOR_REP has unknown value '%s'",inname, icg->t[0].kdata[fi]);
	}

	if (revlookup && !inti3) {
		error("reverse lookup expects .ti3 file as input");
	}


	/* Deal with separation */
	if (dosep) {
		if ((sep_fp = new_icmFileStd_name(&err,sepname,"r")) == NULL)
			error ("Can't open file '%s' (0x%x, '%s')",sepname,err.c,err.m);
	
		if ((sep_icco = new_icc(&err)) == NULL)
			error ("Creation of ICC object failed (0x%x, '%s')",err.c,err.m);
	
		/* Deal with ICC separation */
		if ((rv = sep_icco->read(sep_icco,sep_fp,0)) == 0) {
	
			/* Get a conversion object */
			if ((sep_luo = (icmLuSpace *)sep_icco->get_luobj(sep_icco, icmFwd, icmDefaultIntent, icmSigDefaultData, icmLuOrdNorm)) == NULL) {
					error ("%d, %s",sep_icco->e.c, sep_icco->e.m);
			}
	
			/* Get details of conversion */
			{
				icmCSInfo ini, outi;

				sep_luo->spaces(sep_luo, &ini, &outi, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
				sep_ins = ini.sig;
				sep_inn = ini.nch;
				sep_outs = outi.sig;
			}
			sep_nmask = icx_icc_to_colorant_comb(sep_ins, sep_icco->header->deviceClass);
		}
		if (in_tvenc && sep_ins != icSigRgbData)
			error("Can only use TV encoding on sepration RGB input");
		if (out_tvenc && sep_outs != icSigRgbData)
			error("Can only use TV decoding on sepration RGB output");
	} else {
		if (in_tvenc || out_tvenc)
			warning("TV encoding ignored because there is no sepration file");
	}

	/* Deal with calibration */
	if (calname[0] != '\000') {
		if ((cal = new_xcal()) == NULL)
			error("new_xcal failed");
		if ((cal->read(cal, calname)) != 0)
			error("%s",cal->e.m);
		if (verb) {
			if (applycal == 1)
				printf("Applying calibration curves from '%s'\n",calname);
			else if (applycal == 2)
				printf("Applying inverse calibration curves from '%s'\n",calname);
			else
				printf("Embedding calibration curves from '%s' in output\n",calname);
		}
	}

	/* If the user hasn't overridden it, get any calibration in the .ti2 */
	if (cal == NULL) {			/* No user supplied calibration info */
		int oi, tab;

		oi = icg->get_oi(icg, "CAL");

		for (tab = 0; tab < icg->ntables; tab++) {
			if (icg->t[tab].tt == tt_other && icg->t[tab].oi == oi) {
				break;
			}
		}
		if (tab < icg->ntables) {
			if ((cal = new_xcal()) == NULL) {
				error("new_xcal failed");
			}
			if (cal->read_cgats(cal, icg, tab, inname) != 0)  {
				error("%s",cal->e.m);
			}
		}
	}


	/* Deal with ICC profile */
	if ((icc_fp = new_icmFileStd_name(&err,profname,"r")) == NULL)
		error ("Can't open file '%s' (0x%x, '%s')",profname,err.c,err.m);

	if ((icc_icco = new_icc(&err)) == NULL)
		error ("Creation of ICC object failed (0x%x, '%s')",err.c,err.m);

	/* Read ICC profile */
	if ((rv = icc_icco->read(icc_icco,icc_fp,0)) == 0) {

		/* Embed any calibration in the output if it's present */
		if (cal == NULL) {
			applycal = 0;
			cal = xiccReadCalTag(icc_icco);

			if (verb && cal != NULL)
				printf("Embedding calibration curves from ICC profile in output\n");
		}


		if (revlookup) {
			/* Get a PCS to Device conversion object */
			if ((icc_luo = (icmLuSpace *)icc_icco->get_luobj(icc_icco, icmBwd, intent,
			                           islab ? icSigLabData : icSigXYZData, icmLuOrdNorm)) == NULL) {
				if ((icc_luo = (icmLuSpace *)icc_icco->get_luobj(icc_icco, icmBwd, icmDefaultIntent,
				                       islab ? icSigLabData : icSigXYZData, icmLuOrdNorm)) == NULL)
					error ("%d, %s",icc_icco->e.c, icc_icco->e.m);
			}
		} else {
			/* Get a Device to PCS conversion object */
			if ((icc_luo = (icmLuSpace *)icc_icco->get_luobj(icc_icco, icmFwd, intent,
			                           dolab ? icSigLabData : icSigXYZData, icmLuOrdNorm)) == NULL) {
				if ((icc_luo = (icmLuSpace *)icc_icco->get_luobj(icc_icco, icmFwd, icmDefaultIntent,
				                       dolab ? icSigLabData : icSigXYZData, icmLuOrdNorm)) == NULL)
					error ("%d, %s",icc_icco->e.c, icc_icco->e.m);
			}
		}

		/* Get details of conversion */
		{
			icmCSInfo ini, outi;

			icc_luo->spaces(icc_luo, &ini, &outi, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
			ins = ini.sig;
			inn = ini.nch;
			outs = outi.sig;
			outn = outi.nch;

			alg = icmSynthAlgType((icmLuBase *)icc_luo);
		}

		cnv_nmask = icx_icc_to_colorant_comb(ins, icc_icco->header->deviceClass);

		if (dospec)
			error("Can't lookup spectral values for ICC profile");

		if (tbp[0] >= 0.0) {
			double ss[3], tt[3];
	
			icc_luo->wh_bk_points(icc_luo, wp, bp, NULL);
			if (dolab) {
				icmXYZ2Lab(&icmD50, wp, wp);
				icmXYZ2Lab(&icmD50, bp, bp);
			} else {
				icmLab2XYZ(&icmD50, tbp, tbp);
			}

			/* Create scaling matrix */
			for (j = 0; j < 3; j++) {
				tt[j] = tbp[j] - wp[j];
				ss[j] = bp[j] - wp[j];
			}
			icmRotMat(bpt, ss, tt);

			if (verb) {
				printf("White point = %f %f %f (%s)\n",wp[0],wp[1],wp[2], dolab ? "Lab" : "XYZ");
				printf("Black point = %f %f %f (%s)\n",bp[0],bp[1],bp[2], dolab ? "Lab" : "XYZ");
				printf("Target Black point = %f %f %f (%s)\n",tbp[0],tbp[1],tbp[2], dolab ? "Lab" : "XYZ");
			}
		}

	} else {	/* Not a valid ICC */
		/* Close out the ICC profile */
		icc_icco->del(icc_icco);
		icc_icco = NULL;
		icc_fp->del(icc_fp);
		icc_fp = NULL;
		icc_luo = NULL;
	}

	/* If we don't have an ICC lookup object, look for an MPP */
	if (icc_luo == NULL) {

		if (revlookup)
			error("Reverse lookup using MPP not supported");

		if ((mlu = new_mpp()) == NULL)
			error ("Creation of MPP object failed");

		if ((rv = mlu->read_mpp(mlu, profname)) == 0) {

			/* mlu defaults to absolute XYZ lookup */
			if (dolab) {
				mlu->set_ilob(mlu, icxIT_default, NULL, icxOT_default, NULL, icSigLabData, 0);
			}

			mlu->get_info(mlu, &cnv_nmask, &inn, NULL,
			                   &spec_n, &spec_wl_short, &spec_wl_long, &itype, NULL);
	
			outn = 3;
			outs = dolab ? icSigLabData : icSigXYZData;
	
			if ((ins = icx_colorant_comb_to_icc(cnv_nmask)) == 0)
				error ("Couldn't match MPP mask to valid ICC colorspace");
	
			if (dospec && spec_n <= 0)
				error("Can't lookup spectral values for non-spectral MPP profile");
		} else {
			mlu->del(mlu);
			mlu = NULL;
		}
	}

	/* If we don't have an ICC or MPP lookup object, look for a TI3 */
	if (icc_luo == NULL && mlu == NULL) {
		char *rbuf, *outc;
		char *ti3_bident;
		int ti3_nchan;

		if (revlookup)
			error("Reverse lookup using TI3 as conversion not supported");

		ti3 = new_cgats();			/* Create a CGATS structure */
		ti3->add_other(ti3, "CTI3");/* our special input type is Calibration Target Information 3 */
	
		if (ti3->read_name(ti3, profname))
			error("CGATS file read error : %s",ti3->e.m);
	
		if (ti3->ntables == 0 || ti3->t[0].tt != tt_other || ti3->t[0].oi != 0)
			error ("Profile file '%s' isn't a CTI3 format file",profname);
		if (ti3->ntables != 1)
			error ("Input file '%s' doesn't contain one table",profname);

		if ((ti = ti3->find_kword(ti3, 0, "COLOR_REP")) < 0)
			error("Input file doesn't contain keyword COLOR_REP");
	
		if ((rbuf = strdup(ti3->t[0].kdata[ti])) == NULL)
			error("Malloc failed");
	
		/* Split COLOR_REP into device and PCS space */
		if ((outc = strchr(rbuf, '_')) == NULL)
			error("COLOR_REP '%s' invalid", ti3->t[0].kdata[ti]);
		*outc++ = '\000';
	
		outn = 3;
		if (strcmp(outc, "XYZ") == 0) {
			ti3_isLab = 0;
			outs = icSigXYZData;
		} else if (strcmp(outc, "LAB") == 0) {
			ti3_isLab = 1;
			outs = icSigLabData;
		} else
			error("COLOR_REP '%s' invalid (Neither XYZ nor LAB)", ti3->t[0].kdata[ti]);

		if ((cnv_nmask = icx_char2inkmask(rbuf)) == 0) {
			error ("File '%s' keyword COLOR_REP has unknown device value '%s'",profname,rbuf);
		}

		free(rbuf);

		if ((ins = icx_colorant_comb_to_icc(cnv_nmask)) == 0)
			error ("Couldn't match MPP mask to valid ICC colorspace");

		if ((inn = icmCSSig2nchan(ins)) == 0)
			error ("TI3 Colorspace with unknown number of channels");

		if ((ti3_npat = ti3->t[0].nsets) <= 0)
			error ("No sets of data in reference TI3 file");

		ti3_nchan = icx_noofinks(cnv_nmask);
		ti3_bident = icx_inkmask2char(cnv_nmask, 0); 

		/* Find device fields */
		for (j = 0; j < ti3_nchan; j++) {
			int ii, imask;
			char fname[100];

			imask = icx_index2ink(cnv_nmask, j);
			sprintf(fname,"%s_%s",cnv_nmask == ICX_W || cnv_nmask == ICX_K ? "GRAY" : ti3_bident,
			                      icx_ink2char(imask));

			if ((ii = ti3->find_field(ti3, 0, fname)) < 0)
				error ("Input file doesn't contain field %s",fname);
			if (ti3->t[0].ftype[ii] != r_t)
				error ("Field %s is wrong type - expect float",fname);
			ti3_chix[j] = ii;
		}

		/* Find PCS fields */
		for (j = 0; j < 3; j++) {
			int ii;

			if ((ii = ti3->find_field(ti3, 0, ti3_isLab ? labfname[j] : xyzfname[j])) < 0)
				error ("Input file doesn't contain field %s",ti3_isLab ? labfname[j] : xyzfname[j]);
			if (ti3->t[0].ftype[ii] != r_t)
				error ("Field %s is wrong type - expect float",ti3_isLab ? labfname[j] : xyzfname[j]);
			ti3_pcsix[j] = ii;
		}

		/* Find spectral fields */
		if ((ti = ti3->find_kword(ti3, 0, "SPECTRAL_BANDS")) >= 0) {
			char buf[100];

			spec_n = atoi(ti3->t[0].kdata[ti]);
			if ((ti = ti3->find_kword(ti3, 0, "SPECTRAL_START_NM")) < 0)
				error ("Input file doesn't contain keyword SPECTRAL_START_NM");
			spec_wl_short = atof(ti3->t[0].kdata[ti]);
			if ((ti = ti3->find_kword(ti3, 0, "SPECTRAL_END_NM")) < 0)
				error ("Input file doesn't contain keyword SPECTRAL_END_NM");
			spec_wl_long = atof(ti3->t[0].kdata[ti]);
	
			/* Find the fields for spectral values */
			for (j = 0; j < spec_n; j++) {
				int nm;
		
				/* Compute nearest integer wavelength */
				nm = (int)(spec_wl_short + ((double)j/(spec_n-1.0))
				            * (spec_wl_long - spec_wl_short) + 0.5);
				
				sprintf(buf,"SPEC_%03d",nm);
	
				if ((ti3_spi[j] = ti3->find_field(ti3, 0, buf)) < 0)
					error("Input file doesn't contain field %s",buf);

				if (ti3->t[0].ftype[ti3_spi[j]] != r_t)
					error("Field %s is wrong type - expect float",buf);
			}
		}

		if (dospec && spec_n <= 0)
			error("Can't lookup spectral values for non-spectral TI3 file");

		free(ti3_bident);
	}

	/* Dealy with BT.1886 processing */
	if (bt1886) {
		icmFile *ofp = NULL;	/* Output Color profile file */
		icc *oicco = NULL;		/* Output Profile object */
		icmLuSpace *oluo = NULL;	/* Output Conversion object */
		icmLuMatrix *lu;		/* Input profile lookup */
		double bp[3], rgb[3];

		if (icc_luo == NULL)
			error ("Can only use BT.1886 with an ICC profile");

		/* Check input profile is an RGB matrix profile */
		if (alg != icmMatrixFwdType
		 || ins != icSigRgbData)
			error("BT.1886 mode only works with an RGB matrix input profile");

		lu = (icmLuMatrix *)icc_luo;	/* Safe to coerce - we have checked it's matrix. */

		/* Open up output profile used for BT.1886 black point */
		if ((ofp = new_icmFileStd_name(&err,odispname,"r")) == NULL)
			error ("Can't open file '%s' (0x%x, '%s')",odispname,err.c,err.m);
	
		if ((oicco = new_icc(&err)) == NULL)
			error ("Creation of ICC object failed (0x%x, '%s')",err.c,err.m);
	
		if (oicco->read(oicco,ofp,0))
			error ("Unable to read '%s'",odispname);

		if ((oluo = (icmLuSpace *)oicco->get_luobj(oicco, icmFwd, icRelativeColorimetric,
		                           icSigXYZData, icmLuOrdNorm)) == NULL)
			error("get output ICC lookup object failed: %d, %s",oicco->e.c,oicco->e.m);

		/* We're assuming that the input space has a perfect black point... */

		/* Lookup the output black point in XYZ PCS. We're assuming monotonicity.. */
		bp[0] = bp[1] = bp[2] = 0.0;
		oluo->lookup_fwd(oluo, bp, bp);

		/* Done with output profile */
		oluo->del(oluo);
		oicco->del(oicco);
		ofp->del(ofp);

		bt1886_setup(&bt, &lu->pcswht, bp, outoprop,
		             bt1886 == 1 ? egamma : tgamma, bt1886 == 1 ? 1 : 0);

		if (verb)
			printf("Gamma Curve: Using output black offset proportion %f\n",outoprop);

		if (bt1886 == 1) {		/* Using effective gamma */
			if (verb)
				printf("Gamma Curve: Technical gamma %f used to achieve effective gamma %f\n",
				                                                           bt.gamma, egamma);
		} else {
			if (verb)
				printf("Gamma Curve: Using technical gamma %f\n",bt.gamma);
		}

		
		if (verb) {
			printf("Gamma Curve: target out black rel XYZ = %f %f %f, Lab %f %f %f\n",
			                      bp[0],bp[1],bp[2], bt.outL, bt.tab[0], bt.tab[1]);
			printf("Gamma Curve: Y input offset = %f\n", bt.ingo);
			printf("Gamma Curve: Y output scale = %f\n", bt.outsc);
			printf("Gamma Curve: Y output offset = %f\n", bt.outo);
		}

		/* Check black point now produced by input profile with bt.1886 adjustment */
		rgb[0] = rgb[1] = rgb[2] = 0.0;
		bt1886_fwd_curve(&bt, rgb, rgb);
		lu->core5_fwd(lu, rgb, rgb);
		bt1886_wp_adjust(&bt, rgb, rgb);
		if (verb) printf("BT.1886: check input black point rel. XYZ %f %f %f\n", rgb[0],rgb[1],rgb[2]);
		if (verb > 1) {
			int i, no = 21;

			/* Overral rendering curve from video in to output target */
			printf("BT.1886: overall rendering\n");
			for (i = 0; i < no; i++) {
				double v = i/(no-1.0), vv;
				double vi[3], vo[3], Lab[3];
				double loglog = 0.0;
				
				vi[0] = vi[1] = vi[2] = v;
				bt1886_fwd_curve(&bt, vo, vi);
				lu->core5_fwd(lu, vo, vo);
				bt1886_wp_adjust(&bt, vo, vo);
		
				icmXYZ2Lab(&lu->pcswht, Lab, vo);

				if (v > 1e-9 && vo[1] > 1e-9 && fabs(v - 1.0) > 1e-9)
					loglog = log(vo[1])/log(v);

				printf(" In %5.1f%% -> XYZ in %f -> bt.1886 %f, log/log %.3f, Lab %f %f %f \n",v * 100.0,vi[1],vo[1], loglog, Lab[0], Lab[1], Lab[2]);
			}
		}
	}

	/* Some sanity checking */
	if (tbp[0] >= 0.0 && icc_luo == NULL)
		error("Black scaling only works with ICC profile");

	if (verb) {
//		printf("Random seed is %u\n",seed);

		if (rdlevel > 0.0)
			printf("Adding %.3f%% average deviation %s noise to device values\n",rdlevel * 100.0,unidist ? "uniform" : "normal");
		for (j = 0; j < inn && j < 10; j++) {
			if (chpow[j] != 1.0)
				printf("Applying chan %d power %f\n",j,chpow[j]);
		}
		if (rplevel > 0.0)
			printf("Adding %.3f%% average deviation %s noise to %s PCS values\n",rplevel * 100.0,unidist ? "uniform" : "normal", dolab ? "L*a*b*": "XYZ");
		fflush(stdout);
	}

	/* Setup output cgats file */
	ocg = new_cgats();				/* Create a CGATS structure */
	ocg->add_other(ocg, "CTI3"); 		/* our special type is Calibration Target Information 3 */
	ocg->add_table(ocg, tt_other, 0);	/* Start the first table */

	ocg->add_kword(ocg, 0, "DESCRIPTOR", "Argyll Calibration Target chart information 3",NULL);
	ocg->add_kword(ocg, 0, "ORIGINATOR", "Argyll fakeread", NULL);
	atm[strlen(atm)-1] = '\000';	/* Remove \n from end */
	ocg->add_kword(ocg, 0, "CREATED",atm, NULL);

	/* What sort of device this is */
	if (ti3 != NULL) {
		if ((ti = ti3->find_kword(ti3, 0, "DEVICE_CLASS")) < 0)
			error("Input TI3 doesn't contain keyword DEVICE_CLASS");
		ocg->add_kword(ocg, 0, "DEVICE_CLASS",ti3->t[0].kdata[ti], NULL);	/* Copy */
	} else if (mlu != NULL) {
		/* This is a guess. It may not be correct. */
		if ((cnv_nmask & ICX_ADDITIVE) ^ (cnv_nmask & ICX_INVERTED)) {	
			ocg->add_kword(ocg, 0, "DEVICE_CLASS","DISPLAY", NULL);
			if (nmask == ICX_IRGB)
				warning("It's unusual to have an iRGB display device !");
		} else {
			ocg->add_kword(ocg, 0, "DEVICE_CLASS","OUTPUT", NULL);
			if (nmask == ICX_RGB)
				warning("It's unusual to have an RGB printing device !");
		}
	} else {	/* Assume ICC */
		if (icc_icco->header->deviceClass == icSigDisplayClass) {
			ocg->add_kword(ocg, 0, "DEVICE_CLASS","DISPLAY", NULL);
			if (nmask == ICX_IRGB)
				warning("It's unusual to have an iRGB display device !");
		} else {
			ocg->add_kword(ocg, 0, "DEVICE_CLASS","OUTPUT", NULL);
			if (nmask == ICX_RGB)
				warning("It's unusual to have an RGB printing device !");
		}
	}

	if ((ti = icg->find_kword(icg, 0, "SINGLE_DIM_STEPS")) >= 0)
		ocg->add_kword(ocg, 0, "SINGLE_DIM_STEPS",icg->t[0].kdata[ti], NULL);

	if ((ti = icg->find_kword(icg, 0, "COMP_GREY_STEPS")) >= 0)
		ocg->add_kword(ocg, 0, "COMP_GREY_STEPS",icg->t[0].kdata[ti], NULL);

	if ((ti = icg->find_kword(icg, 0, "MULTI_DIM_STEPS")) >= 0)
		ocg->add_kword(ocg, 0, "MULTI_DIM_STEPS",icg->t[0].kdata[ti], NULL);

	if ((ti = icg->find_kword(icg, 0, "FULL_SPREAD_PATCHES")) >= 0)
		ocg->add_kword(ocg, 0, "FULL_SPREAD_PATCHES",icg->t[0].kdata[ti], NULL);
	
	if ((ti = icg->find_kword(icg, 0, "DARK_REGION_EMPHASIS")) >= 0)
		ocg->add_kword(ocg, 0, "DARK_REGION_EMPHASIS",icg->t[0].kdata[ti], NULL);

	if (icc_luo == NULL) {	/* If MPP profile, we know what the target instrument is */
		ocg->add_kword(ocg, 0, "TARGET_INSTRUMENT", inst_name(itype) , NULL);
	}

	/* Fields we want */
	ocg->add_field(ocg, 0, "SAMPLE_ID", nqcs_t);

	if ((si = icg->find_field(icg, 0, "SAMPLE_ID")) < 0)
		error ("Input file doesn't contain field SAMPLE_ID");

	{
		int i, j, ii;
		int chix[ICX_MXINKS];	/* Device chanel indexes */
		int pcsix[3];			/* PCS chanel indexes (for revlookup) */
		char *ident, *bident;
		int nsetel = 0;
		cgats_set_elem *setel;	/* Array of set value elements */

		nchan = icx_noofinks(nmask);				/* No. device channels */
		ident = icx_inkmask2char(nmask, 1); 
		bident = icx_inkmask2char(nmask, 0); 

		/* Sanity check what we're going to do */
		if (dosep) {

			/* Check if sep ICC input is compatible with .ti1 */
			if (nmask == ICX_W && sep_ins == icSigRgbData)
				gfudge = 1;
			else if (nmask == ICX_K && sep_ins == icSigCmykData)
				gfudge = 2;
			else if (icx_colorant_comb_match_icc(nmask, sep_ins) == 0) {
				error("Separation ICC device space '%s' doesn't match TI1 '%s'",
				       icm2str(icmColorSpaceSig, sep_ins),
				       ident);	/* Should free(). */
			}

			/* Check if separation ICC output is compatible with ICC/MPP/TI3 conversion */ 
			if (icc_luo != NULL) {
				/* Check if icc is compatible with .ti1 */
				if (sep_outs != ins)
					error("ICC device space '%s' doesn't match Separation ICC '%s'",
					       icm2str(icmColorSpaceSig, ins),
					       icm2str(icmColorSpaceSig, sep_outs));
			} else if (mlu != NULL) {
				/* Check if mpp is compatible with .ti1 */
				if (icx_colorant_comb_match_icc(cnv_nmask, sep_outs) == 0)
					error("MPP device space '%s' doesn't match Separation ICC '%s'",
					       icx_inkmask2char(cnv_nmask, 1),	/* Should free(). */
					       icm2str(icmColorSpaceSig, sep_outs));
			} else if (ti3 != NULL) {
				/* Check if .ti3 is compatible with .ti1 */
				if (icx_colorant_comb_match_icc(cnv_nmask, sep_outs) == 0)
					error("TI3 device space '%s' doesn't match Separation ICC '%s'",
					       icx_inkmask2char(cnv_nmask, 1),	/* Should free(). */
					       icm2str(icmColorSpaceSig, sep_outs));
			}
		} else if (icc_luo != NULL) {
			/* Check if icc is compatible with .ti1 */
			if (nmask == ICX_W && ins == icSigRgbData)
				gfudge = 1;
			else if (nmask == ICX_K && ins == icSigCmykData)
				gfudge = 2;		/* Should allow for other colorant combo's that include black */
			else {
				if (!revlookup) {
					if (icx_colorant_comb_match_icc(nmask, ins) == 0)
						error("ICC device space '%s' doesn't match TI1 '%s'",
						       icm2str(icmColorSpaceSig, ins),
						       ident);	// Should free().
				} else {
					if (icx_colorant_comb_match_icc(nmask, outs) == 0)
						error("ICC device space '%s' doesn't match TI1 '%s'",
						       icm2str(icmColorSpaceSig, ins),
						       ident);	// Should free().

				}
			}
		} else if (mlu != NULL) {
			/* Check if mpp is compatible with .ti1 */
			if (nmask == ICX_W && (cnv_nmask == ICX_RGB || cnv_nmask == ICX_IRGB))
				gfudge = 1;
			else if (nmask == ICX_K && (cnv_nmask & ICX_BLACK))
				gfudge = 2;
			else if (cnv_nmask != nmask)
				error("MPP device space '%s' doesn't match TI1 '%s'",
				       icx_inkmask2char(cnv_nmask, 1), ident);	// Should free().
		} else if (ti3 != NULL) {
			/* Check if .ti3 is compatible with .ti1 */
			if (nmask == ICX_W && (cnv_nmask == ICX_RGB || cnv_nmask == ICX_IRGB))
				gfudge = 1;
			else if (nmask == ICX_K && (cnv_nmask & ICX_BLACK))
				gfudge = 2;
			else if (cnv_nmask != nmask)
				error("TI3 device space '%s' doesn't match TI1 '%s'",
				       icx_inkmask2char(cnv_nmask, 1), ident);	// Should free().
		}

		if ((ii = icg->find_kword(icg, 0, "TOTAL_INK_LIMIT")) >= 0)
			ocg->add_kword(ocg, 0, "TOTAL_INK_LIMIT",icg->t[0].kdata[ii], NULL);
		if ((ii = icg->find_kword(icg, 0, "FINAL_TOTAL_INK_LIMIT")) >= 0)
			ocg->add_kword(ocg, 0, "FINAL_TOTAL_INK_LIMIT",icg->t[0].kdata[ii], NULL);
		if ((ii = icg->find_kword(icg, 0, "TEXT_SPACER_TOTAL_INK_LIMIT")) >= 0)
			ocg->add_kword(ocg, 0, "TEXT_SPACER_TOTAL_INK_LIMIT",icg->t[0].kdata[ii], NULL);
		if ((ii = icg->find_kword(icg, 0, "FINAL_TEXT_SPACER_TOTAL_INK_LIMIT")) >= 0)
			ocg->add_kword(ocg, 0, "FINAL_TEXT_SPACER_TOTAL_INK_LIMIT",icg->t[0].kdata[ii], NULL);

		nsetel += 1;		/* For id */
		nsetel += nchan;	/* For device values */
		nsetel += 3;		/* For XYZ/Lab */

		/* Locate device fields in source file, and add to output file */
		for (j = 0; j < nchan; j++) {
			int imask;
			char fname[100];

			imask = icx_index2ink(nmask, j);
			sprintf(fname,"%s_%s",nmask == ICX_W || nmask == ICX_K ? "GRAY" : bident,
			                      icx_ink2char(imask));

			if ((ii = icg->find_field(icg, 0, fname)) < 0)
				error ("Input file doesn't contain field %s",fname);
			if (icg->t[0].ftype[ii] != r_t)
				error ("Field %s is wrong type - expect float",fname);
	
			ocg->add_field(ocg, 0, fname, r_t);
			chix[j] = ii;
		}

		/* Find PCS fields if doing reverse lookup */
		if (revlookup) {
			for (j = 0; j < 3; j++) {
				int ii;
	
				if ((ii = icg->find_field(icg, 0, islab ? labfname[j] : xyzfname[j])) < 0)
					error ("Input file doesn't contain field %s",islab ? labfname[j] : xyzfname[j]);
				if (icg->t[0].ftype[ii] != r_t)
					error ("Field %s is wrong type - expect float",islab ? labfname[j] : xyzfname[j]);
				pcsix[j] = ii;
			}
		}

		/* Add PCS fields */
		for (j = 0; j < 3; j++) {
			ocg->add_field(ocg, 0, dolab ? labfname[j] : xyzfname[j], r_t);
		}

		/* If we have spectral information, output it too */
		if (dospec > 0 && spec_n > 0) {
			char buf[100];

			nsetel += spec_n;		/* Spectral values */
			sprintf(buf,"%d", spec_n);
			ocg->add_kword(ocg, 0, "SPECTRAL_BANDS",buf, NULL);
			sprintf(buf,"%f", spec_wl_short);
			ocg->add_kword(ocg, 0, "SPECTRAL_START_NM",buf, NULL);
			sprintf(buf,"%f", spec_wl_long);
			ocg->add_kword(ocg, 0, "SPECTRAL_END_NM",buf, NULL);

			/* Generate fields for spectral values */
			for (i = 0; i < spec_n; i++) {
				int nm;
		
				/* Compute nearest integer wavelength */
				nm = (int)(spec_wl_short + ((double)i/(spec_n-1.0))
				            * (spec_wl_long - spec_wl_short) + 0.5);
				
				sprintf(buf,"SPEC_%03d",nm);
				ocg->add_field(ocg, 0, buf, r_t);
			}
		}

		{
			char fname[100];
			sprintf(fname, dolab ? "%s_LAB" : "%s_XYZ", ident);
			ocg->add_kword(ocg, 0, "COLOR_REP", fname, NULL);
		}

		if ((setel = (cgats_set_elem *)malloc(sizeof(cgats_set_elem) * nsetel)) == NULL)
			error("Malloc failed!");

		/* Read all the device test patches in, convert them to PCS, */
		/* and write them out. */
		if (!revlookup) {
			for (i = 0; i < npat; i++) {
				int k = 0;
				char *id;
				double odev[ICX_MXINKS], dev[ICX_MXINKS], sep[ICX_MXINKS], PCS[3];
				xspect out;
				double qscale = (1 << qbits) - 1.0;
	
				for (j = 0; j < nchan; j++) {
					double dv = *((double *)icg->t[0].fdata[i][chix[j]]) / 100.0;
					if (qbits > 0) {
						double vr;
						dv *= qscale;
						vr = floor(dv + 0.5);
						if ((vr - dv) == 0.5 && (((int)vr) & 1) != 0) /* Round to even */
							vr -= 1.0;
						dv = vr/qscale;
					}
					odev[j] = dev[j] = sep[j] = dv;
				}
	
				if (gfudge) {
					int nch;
	
					if (dosep)		/* Figure number of channels into conversion */
						nch = sep_inn;
					else
						nch = inn;
	
					if (gfudge == 1) { 	/* Convert W -> RGB */
						double wval = dev[0];
						for (j = 0; j < nch; j++)
							dev[j] = sep[j] = wval; 
	
					} else {			/* Convert K->xxxK */
						int kch;
						int inmask;
						double kval = dev[0];
	
						if (dosep)		/* Figure number of channels into conversion */
							inmask = sep_nmask;
						else
							inmask = cnv_nmask;
	
						if (inmask == 0)
							error("Input colorspace ambiguous - can't determine if it has black");
	
						if ((kch = icx_ink2index(inmask, ICX_BLACK)) == -1)
							error("Can't find black colorant for K fudge");
						for (j = 0; j < nch; j++) {
							if (j == kch)
								dev[j] = sep[j] = kval; 
							else
								dev[j] = sep[j] = 0.0; 
						}
					}
				}
	
				if (dosep) {
					if (in_tvenc != 0) {
						if (in_tvenc == 1) {			/* Video 16-235 range */
							icmRGB_2_VidRGB(dev, dev);
						} else if (in_tvenc == 2) {		/* Rec601 YCbCr */
							icmRec601_RGBd_2_YPbPr(dev, dev);
							icmRecXXX_YPbPr_2_YCbCr(dev, dev);
						} else if (in_tvenc == 3) {		/* Rec709 YCbCr */
							icmRec709_RGBd_2_YPbPr(dev, dev);
							icmRecXXX_YPbPr_2_YCbCr(dev, dev);
						} else if (out_tvenc == 4) {		/* Rec709 1250/50/2:1 YCbCr */
							icmRec709_50_RGBd_2_YPbPr(dev, dev);
							icmRecXXX_YPbPr_2_YCbCr(dev, dev);
						} else if (out_tvenc == 5) {		/* Rec2020 Non-constant Luminance YCbCr */
							icmRec2020_NCL_RGBd_2_YPbPr(dev, dev);
							icmRecXXX_YPbPr_2_YCbCr(dev, dev);
						} else if (out_tvenc == 6) {		/* Rec2020 Non-constant Luminance YCbCr */
							icmRec2020_CL_RGBd_2_YPbPr(dev, dev);
							icmRecXXX_YPbPr_2_YCbCr(dev, dev);
						}
					}
	
					if (sep_luo->lookup_fwd(sep_luo, sep, dev) & icmPe_lurv_err)
						error ("%d, %s",icc_icco->e.c,icc_icco->e.m);
	
					if (out_tvenc != 0) {
						if (out_tvenc == 1) {				/* Video 16-235 range */
							icmVidRGB_2_RGB(sep, sep);
						} else if (out_tvenc == 2) {		/* Rec601 YCbCr */
							icmRecXXX_YCbCr_2_YPbPr(sep, sep);
							icmRec601_YPbPr_2_RGBd(sep, sep);
						} else if (out_tvenc == 3) {		/* Rec709 1150/60/2:1 YCbCr */
							icmRecXXX_YCbCr_2_YPbPr(sep, sep);
							icmRec709_YPbPr_2_RGBd(sep, sep);
						} else if (out_tvenc == 4) {		/* Rec709 1250/50/2:1 YCbCr */
							icmRecXXX_YCbCr_2_YPbPr(sep, sep);
							icmRec709_50_YPbPr_2_RGBd(sep, sep);
						} else if (out_tvenc == 5) {		/* Rec2020 Non-constant Luminance YCbCr */
							icmRecXXX_YCbCr_2_YPbPr(sep, sep);
							icmRec2020_NCL_YPbPr_2_RGBd(sep, sep);
						} else if (out_tvenc == 6) {		/* Rec2020 Non-constant Luminance YCbCr */
							icmRecXXX_YCbCr_2_YPbPr(sep, sep);
							icmRec2020_CL_YPbPr_2_RGBd(sep, sep);
						}
					}
				}
	
				/* Do calibration */
				if (applycal && cal != NULL) {
					if (applycal == 1)
						cal->interp(cal, sep, sep);
					else if (applycal == 2) {
						if (cal->inv_interp(cal, sep, sep))
							warning("Inverse calibration of patch %d failed",i+1); 
					}
				}
	
				/* Add randomness and non-linearity to device values. */
				/* rdlevel = avg. dev. */
				/* Note dev/sep is 0-1.0 at this stage */
				for (j = 0; j < inn; j++) {
					double dv = sep[j];
					if (rdlevel > 0.0) {
						double rr;
						if (unidist)
							rr =  d_rand(-2.0 * rdlevel, 2.0 * rdlevel);
						else
							rr = 1.2533 * rdlevel * norm_rand();
						dv += rr;
						if (dv < 0.0)
							dv = 0.0;
						else if (dv > 1.0)
							dv = 1.0;
					}
					if (j < 10 && chpow[j] != 1.0) {
						dv = pow(dv, chpow[j]);
					}
					sep[j] = dv;
				}
	
				/* Do color conversion */
				if (icc_luo != NULL) {
					if (bt1886) {
						icmLuMatrix *lu = (icmLuMatrix *)icc_luo;    /* Safe to coerce */
	//printf("Matrix dev in: %s\n",icmPdv(inn, sep));
						bt1886_fwd_curve(&bt, sep, sep);
	//printf("Matrix after bt1886 curve: %s\n",icmPdv(inn, sep));
						lu->core5_fwd(lu, PCS, sep);
	//printf("Matrix after matrix XYZ %s Lab %s\n",icmPdv(3, PCS), icmPLab(PCS));
						bt1886_wp_adjust(&bt, PCS, PCS);
	//printf("Matrix after bt1186 wp adj. XYZ %s Lab %s\n",icmPdv(3, PCS), icmPLab(PCS));
						lu->output_pch_fwd(lu, PCS, PCS);
						lu->output_fmt_fwd(lu, PCS, PCS);
	//printf("Matrix after abs %s\n",icmPdv(3, PCS));
					} else {
						if (icc_luo->lookup_fwd(icc_luo, PCS, sep) & icmPe_lurv_err)
							error ("%d, %s",icc_icco->e.c,icc_icco->e.m);
					}
	
					if (tbp[0] >= 0) {	/* Doing black point scaling */
	
						for (j = 0; j < 3; j++)
							PCS[j] -= wp[j];
						icmMulBy3x3(PCS, bpt, PCS);
						for (j = 0; j < 3; j++)
							PCS[j] += wp[j];
					}
		
				} else if (mlu != NULL) {
					mlu->lookup(mlu, PCS, sep);
					if (dospec && spec_n > 0) {
						mlu->lookup_spec(mlu, &out, sep);
					}
				} else if (ti3 != NULL) {
					int m;
					double bdif = 1e6;
					int bix = -1;
	
					/* Search for the closest device values in TI3 file */
					for (m = 0; m < ti3_npat; m++) {
						double dif;
	
						for (dif = 0.0, j = 0; j < nchan; j++) {
							double xx;
	
							xx = (*((double *)ti3->t[0].fdata[m][ti3_chix[j]]) / 100.0) - sep[j];
							dif += xx * xx;
						}
						if (dif < bdif) {
							bdif = dif;
							bix = m;
						}
					}
					/* Copy best value over */
					if (!dosep)		/* Doesn't make sense for separation ??? */
						for (j = 0; j < nchan; j++) {
							dev[j] = *((double *)ti3->t[0].fdata[bix][ti3_chix[j]]) / 100.0;
						}
					for (j = 0; j < 3; j++) {
						PCS[j] = *((double *)ti3->t[0].fdata[bix][ti3_pcsix[j]]);
					}
					if (ti3_isLab && !dolab) {	/* Convert Lab to XYZ */
						icmLab2XYZ(&icmD50, PCS, PCS);
					} else if (!ti3_isLab && dolab) {	/* Convert XYZ to Lab */
						icmXYZ2Lab(&icmD50, PCS, PCS);
					} else if (!ti3_isLab) {		/* Convert XYZ100 to XYZ1 */
						PCS[0] /= 100.0;
						PCS[1] /= 100.0;
						PCS[2] /= 100.0;
					}
					if (dospec && spec_n > 0) {
						for (j = 0; j < spec_n; j++) {
							out.spec[j] = *((double *)ti3->t[0].fdata[bix][ti3_spi[j]]);
						}
					}
				}
	
				id = ((char *)icg->t[0].fdata[i][si]);
				setel[k++].c = id;
				
				for (j = 0; j < nchan; j++)
					setel[k++].d = 100.0 * odev[j];
	
				if (dolab == 0) {
					PCS[0] *= 100.0;
					PCS[1] *= 100.0;
					PCS[2] *= 100.0;
				}
	
				/* Add randomness. rplevel is avg. dev. */
				/* Note PCS is 0..100 XYZ or Lab at this point */
				/* Adding uniform error to XYZ produces unreasonable */
				/* bumpiness near black, so scale it by Y */
				if (rplevel > 0.0) {
					double opcs[3], ll = 1.0;
					if (!dolab)
						ll = 0.01 * PCS[1];
					for (j = 0; j < 3; j++) {
						double dv = PCS[j];
						double rr;
						opcs[j] = dv;
						if (unidist)
							rr = 100.0 * d_rand(-2.0 * rplevel, 2.0 * rplevel);
						else
							rr = 100.0 * 1.2533 * rplevel * norm_rand();
						dv += ll * rr;
	
						/* Don't let L*, X, Y or Z go negative */
						if ((!dolab || j == 0) && dv < 0.0)
							dv = 0.0;
						PCS[j] = dv;
					}
	//printf("~1 pcs %f %f %f -> %f %f %f\n", opcs[0], opcs[1], opcs[2], PCS[0], PCS[1], PCS[2]);
				}
	
				setel[k++].d = PCS[0];
				setel[k++].d = PCS[1];
				setel[k++].d = PCS[2];
	
				if (dospec && spec_n > 0) {
					for (j = 0; j < spec_n; j++) {
						setel[k++].d = 100.0 * out.spec[j];
					}
				}
	
				ocg->add_setarr(ocg, 0, setel);
			}

		/* Do reverse (PCS -> device) lookup */
		} else {

			/* Read all the PCS test patches in, convert them to device values, */
			/* and write them out. */
			for (i = 0; i < npat; i++) {
				int k = 0;
				char *id;
				double pcs[3];
				double dev[ICX_MXINKS];
	
				/* read PCS values */
				for (j = 0; j < 3; j++) {
					double pv = *((double *)icg->t[0].fdata[i][pcsix[j]]);
					if (!islab)
						pv /= 100.0;
					pcs[j] = pv;
				}
	
				/* Do PCS to device color conversion */
				if (icc_luo->lookup_fwd(icc_luo, dev, pcs) & icmPe_lurv_err)
						error ("%d, %s",icc_icco->e.c,icc_icco->e.m);
	
				/* Write the values */
				id = ((char *)icg->t[0].fdata[i][si]);
				setel[k++].c = id;
				
				for (j = 0; j < nchan; j++)
					setel[k++].d = 100.0 * dev[j];
	
				for (j = 0; j < 3; j++) {
					double pv = pcs[j]; 
					if (!islab)
						pv *= 100.0;
					setel[k++].d = pv;
				}
	
				ocg->add_setarr(ocg, 0, setel);
			}
		}

		free(setel);
		free(ident);
		free(bident);
	}

	if (sep_luo != NULL) {
		sep_luo->del(sep_luo);
		sep_icco->del(sep_icco);
		sep_fp->del(sep_fp);
	}

	/* If there is a calibration, append it to the .ti3 file */
	if (cal != NULL) {
		if (cal->write_cgats(cal, ocg) != 0)
			error("Writing cal error : %s",cal->e.m);
	}
	
	if (cal != NULL)
		cal->del(cal);


	if (icc_luo != NULL) {
		/* Cleanup ICC profile */
		icc_luo->del(icc_luo);
		icc_icco->del(icc_icco);
		icc_fp->del(icc_fp);
	} else if (mlu != NULL) {
		mlu->del(mlu);
	} else if (ti3 != NULL) {
		ti3->del(ti3);
	}

	if (ocg->write_name(ocg, outname))
		error("Write error : %s",ocg->e.m);

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

	return 0;
}




