/* */
/*  Little cms - profiler construction set */
/*  Copyright (C) 1998-2001 Marti Maria <marti@littlecms.com> */
/* */
/* THIS SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY */
/* WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. */
/* */
/* IN NO EVENT SHALL MARTI MARIA BE LIABLE FOR ANY SPECIAL, INCIDENTAL, */
/* INDIRECT OR CONSEQUENTIAL DAMAGES OF ANY KIND, */
/* OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, */
/* WHETHER OR NOT ADVISED OF THE POSSIBILITY OF DAMAGE, AND ON ANY THEORY OF */
/* LIABILITY, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE */
/* OF THIS SOFTWARE. */
/* */
/* This file is free software; you can redistribute it and/or modify it */
/* under the terms of the GNU General Public License as published by */
/* the Free Software Foundation; either version 2 of the License, or */
/* (at your option) any later version. */
/* */
/* This program is distributed in the hope that it will be useful, but */
/* WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU */
/* General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program; if not, write to the Free Software */
/* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */
/* */
/* As a special exception to the GNU General Public License, if you */
/* distribute this file as part of a program that contains a */
/* configuration script generated by Autoconf, you may include it under */
/* the same distribution terms that you use for the rest of that program. */
/* */
/* Version 1.09a */


#include "lcmsprf.h"


LPGAMMATABLE cdecl cmsxEstimateGamma(LPSAMPLEDCURVE X, LPSAMPLEDCURVE Y, int nResultingPoints);
void		 cdecl cmsxCompleteLabOfPatches(LPMEASUREMENT m, SETOFPATCHES Valids, int Medium);

void cdecl cmsxComputeLinearizationTables(LPMEASUREMENT m, 
                                    int ColorSpace, 
                                    LPGAMMATABLE Lin[3],
									int nResultingPoints,
									int Medium);
                                          
                                          
void cdecl cmsxApplyLinearizationTable(double In[3], 
                                       LPGAMMATABLE Gamma[3], 
                                       double Out[3]);

void cdecl cmsxApplyLinearizationGamma(WORD In[3], LPGAMMATABLE Gamma[3], WORD Out[3]);



/* ------------------------------------------------------------- Implementation */


#define EPSILON	0.00005
#define LEVENBERG_MARQUARDT_ITERATE_MAX  150

/* In order to track linearization tables, we use following procedure */
/* */
/* We first assume R', G' and B' does exhibit a non-linear behaviour */
/* that can be separated for each channel as Yr(R'), Yg(G'), Yb(B') */
/* This is the shaper step */
/* */
/*  R = Lr(R') */
/*  G = Lg(G') */
/*  B = Lb(B')   (0.0) */
/* */
/* After this step, RGB is converted to XYZ by a matrix multiplication */
/* */
/*  |X|       |R| */
/*  |Y| = [M]·|G| */
/*  |Z|       |B|    (1.0) */
/* */
/* In order to extract Lr,Lg,Lb tables, we are interested only on Y part  */
/*  */
/*  Y = (m1 * R + m2 * G + m3 * B)  (1.1) */
/* */
/* The total intensity for maximum RGB = (1, 1, 1) should be 1, */
/* */
/*  1 = m1 * 1 + m2 * 1 + m3 * 1, so */
/* */
/*  m1 + m2 + m3 = 1.0		(1.2) */
/* */
/* We now impose that for neutral (gray) patches, RGB components must be equal */
/* */
/*	R = G = B = Gray  */
/*  */
/* So, substituting in (1.1): */
/*  */
/*  Y = (m1 + m2 + m3) Gray */
/*  */
/* and for (1.2), (m1+m2+m3) = 1, so */
/* */
/*  Y = Gray = Lr(R') = Lg(G') = Lb(B') */
/* */
/* That is, after prelinearization, RGB of gray patches should give  */
/* same values for R, G and B. And this value is Y. */
/* */
/* */


static
LPSAMPLEDCURVE NormalizeTo(LPSAMPLEDCURVE X, double N, BOOL lAddEndPoint)
{
		int i, nItems;
		LPSAMPLEDCURVE XNorm;
		
		nItems = X ->nItems;
		if (lAddEndPoint) nItems++;

		XNorm = cmsAllocSampledCurve(nItems);

		for (i=0; i < X ->nItems; i++) {

			XNorm ->Values[i] = X ->Values[i] / N;  			
		}
		
		if (lAddEndPoint)
			XNorm -> Values[X ->nItems] = 1.0;

		return XNorm;	
}


/* */
/* ------------------------------------------------------------------------------ */
/* */
/* Our Monitor model. We assume gamma has a general expression of */
/* */
/*  Fn(x) = (Gain * x + offset) ^ gamma | for x >= 0 */
/*	Fn(x) = 0							| for x < 0 */
/*	 */
/*	First partial derivatives are */
/* */
/*  dFn/dGamma  = Fn * ln(Base) */
/*  dFn/dGain   = gamma * x * ((Gain * x + Offset) ^ (gamma -1)) */
/*	dFn/dOffset = gamma * ((Gain * x + Offset) ^ (gamma -1)) */
/* */

static 
void GammaGainOffsetFn(double x, double *a, double *y, double *dyda, int na)
{
    double Gamma,Gain,Offset;
	double Base;

    Gamma  = a[0];
    Gain   = a[1];
    Offset = a[2];

    Base = Gain * x + Offset; 	

	if (Base < 0) {

		Base = 0.0;
		*y = 0.0;    
		dyda[0] = 0.0;
		dyda[1] = 0.0;
		dyda[2] = 0.0;


	} else {
		
	
		/* The function itself */
		*y = pow(Base, Gamma);

		/* dyda[0] is partial derivative across Gamma */
		dyda[0] = *y * log(Base);
	    	    
		/* dyda[1] is partial derivative across gain */
		dyda[1] = (x * Gamma) * pow(Base, Gamma-1.0);

		/* dyda[2] is partial derivative across offset */
		dyda[2] =     Gamma * pow(Base, Gamma-1.0);
	}
} 


/* Fit curve to our gamma-gain-offset model. */

static
BOOL OneTry(LPSAMPLEDCURVE XNorm, LPSAMPLEDCURVE YNorm, double a[])
{
	LCMSHANDLE h;
	double ChiSq, OldChiSq;		
	int i;
	BOOL Status = true;

		/* initial guesses */

	    a[0] = 3.0;			/* gamma */
	    a[1] = 4.0;			/* gain */
	    a[2] = 6.0;			/* offset */
		a[3] = 0.0;			/* Thereshold */
		a[4] = 0.0;			/* Black */

							
		/* Significance = 0.02 gives good results */

		h = cmsxLevenbergMarquardtInit(XNorm, YNorm,  0.02, a, 3, GammaGainOffsetFn);							
		if (h == NULL) return false;


		OldChiSq = cmsxLevenbergMarquardtChiSq(h);

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

			if (!cmsxLevenbergMarquardtIterate(h)) {
				Status = false;
				break;
			}

			ChiSq = cmsxLevenbergMarquardtChiSq(h);
		
			if(OldChiSq != ChiSq && (OldChiSq - ChiSq) < EPSILON)
				break;

			OldChiSq = ChiSq;
		}
		
		cmsxLevenbergMarquardtFree(h);

		return Status;
}

/* Tries to fit gamma as per IEC 61966-2.1 using Levenberg-Marquardt method */
/*  */
/* Y = (aX + b)^Gamma | X >= d */
/* Y = cX             | X < d */

LPGAMMATABLE cmsxEstimateGamma(LPSAMPLEDCURVE X, LPSAMPLEDCURVE Y, int nResultingPoints)
{
	double a[5];
	LPSAMPLEDCURVE XNorm, YNorm;	
	double e, Max;


		/* Coarse approximation, to find maximum.  */
	    /* We have only a portion of curve. It is likely */
	    /* maximum will not fall on exactly 100. */

		if (!OneTry(X, Y, a)) 
			return false;

		/* Got parameters. Compute maximum. */
		e = a[1]* 255.0 + a[2];
		if (e < 0) return false;
		Max = pow(e, a[0]);
		

		/* Normalize values to maximum */
		XNorm = NormalizeTo(X, 255.0, false);
		YNorm = NormalizeTo(Y, Max, false);

		/* Do the final fitting  */
		if (!OneTry(XNorm, YNorm, a))
				return false;
				
		/* Type 3 = IEC 61966-2.1 (sRGB) */
        /* Y = (aX + b)^Gamma | X >= d */
        /* Y = cX             | X < d */
		return  cmsBuildParametricGamma(nResultingPoints, 3, a);
}





/* A dumb bubble sort */

static
void Bubble(LPSAMPLEDCURVE C, LPSAMPLEDCURVE L)
{
#define SWAP(a, b)      { tmp = (a); (a) = (b); (b) = tmp; }

        BOOL lSwapped;
        int i, nItems;
        double tmp;

		nItems = C -> nItems;
        do {
                lSwapped = false;

                for (i= 0; i <  nItems - 1; i++) {

                        if (C->Values[i] > C->Values[i+1]) {

                                SWAP(C->Values[i], C->Values[i+1]);
								SWAP(L->Values[i], L->Values[i+1]);                            
                                lSwapped = true;
                        }
                }

        } while (lSwapped);

#undef SWAP
}



/* Check for monotonicity. Force it if is not the case. */

static
void CheckForMonotonicSampledCurve(LPSAMPLEDCURVE t)
{
    int n = t ->nItems;
    int i;
	double last;

    last = t ->Values[n-1];
    for (i = n-2; i >= 0; --i) {
        
        if (t ->Values[i] > last)

                t ->Values[i] = last;
        else
                last = t ->Values[i];

    }
    
}

/* The main gamma inferer. Tries first by gamma-gain-offset,  */
/* if not proper reverts to curve guessing. */

static
LPGAMMATABLE BuildGammaTable(LPSAMPLEDCURVE C, LPSAMPLEDCURVE L, int nResultingPoints)
{
	LPSAMPLEDCURVE Cw, Lw, Cn, Ln;
	LPSAMPLEDCURVE out;
	LPGAMMATABLE Result;
	double Lmax, Lend, Cmax;

	/* Try to see if it can be fitted 	 */
	Result = cmsxEstimateGamma(C, L, nResultingPoints);  
	if (Result)
		return Result;
	

	/* No... build curve from scratch. Since we have not */
	/* endpoints, a coarse linear extrapolation should be */
	/* applied in order to get the expected maximum. */

	Cw = cmsDupSampledCurve(C);
	Lw = cmsDupSampledCurve(L);
	
	Bubble(Cw, Lw);

    /* Get endpoint */
	Lmax = Lw->Values[Lw ->nItems - 1];
	Cmax = Cw->Values[Cw ->nItems - 1];

	/* Linearly extrapolate */
	Lend = (255 * Lmax) / Cmax;

	Ln = NormalizeTo(Lw, Lend, true);
	Cn = NormalizeTo(Cw, 255.0, true);

	cmsFreeSampledCurve(Cw);
	cmsFreeSampledCurve(Lw);

	/* Add endpoint */
	out = cmsJoinSampledCurves(Cn, Ln,  nResultingPoints);
	
	cmsFreeSampledCurve(Cn);
	cmsFreeSampledCurve(Ln);
	
	CheckForMonotonicSampledCurve(out);

	cmsSmoothSampledCurve(out, nResultingPoints*4.);
	cmsClampSampledCurve(out, 0, 1.0);			
	
	Result = cmsConvertSampledCurveToGamma(out, 1.0);    

	cmsFreeSampledCurve(out);
	return Result;
}




void cmsxCompleteLabOfPatches(LPMEASUREMENT m, SETOFPATCHES Valids, int Medium)
{
	LPPATCH White;	
	cmsCIEXYZ WhiteXYZ;
	int i;

	if (Medium == MEDIUM_REFLECTIVE_D50)
	{
		WhiteXYZ.X = D50X * 100.;
		WhiteXYZ.Y = D50Y * 100.;
		WhiteXYZ.Z = D50Z * 100.;
	}
	else {

		White = cmsxPCollFindWhite(m, Valids, NULL);
		if (!White) return;

		WhiteXYZ = White ->XYZ;
	}

	/* For all patches with XYZ and without Lab, add Lab values. */
	/* Transmissive profiles does need to locate its own white */
	/* point for device gray. Reflective does use D50 */

	 for (i=0; i < m -> nPatches; i++) {

       if (Valids[i]) {

        LPPATCH p = m -> Patches + i;

		if ((p ->dwFlags & PATCH_HAS_XYZ) && 
			(!(p ->dwFlags & PATCH_HAS_Lab) || (Medium == MEDIUM_TRANSMISSIVE))) {

			cmsXYZ2Lab(&WhiteXYZ, &p->Lab, &p->XYZ);
			p -> dwFlags |= PATCH_HAS_Lab;
		}
	   }
	 }
}


/* Compute linearization tables, trying to fit in a pure  */
/* exponential gamma. If gamma cannot be accurately infered, */
/* then does build a smooth, monotonic curve that does the job. */

void cmsxComputeLinearizationTables(LPMEASUREMENT m, 
                                    int ColorSpace, 
                                    LPGAMMATABLE Lin[3],
									int nResultingPoints,
									int Medium)                                    
                                    
{
    LPSAMPLEDCURVE R, G, B, L;
	LPGAMMATABLE gr, gg, gb;
    SETOFPATCHES Neutrals;
    int nGrays;
    int i;
	      
	/* We need Lab for grays. */
	cmsxCompleteLabOfPatches(m, m->Allowed, Medium);

    /* Add neutrals, normalize to max */
    Neutrals = cmsxPCollBuildSet(m, false);
    cmsxPCollPatchesNearNeutral(m, m ->Allowed, 15, Neutrals);	

    nGrays = cmsxPCollCountSet(m, Neutrals);
    
    R = cmsAllocSampledCurve(nGrays);   
    G = cmsAllocSampledCurve(nGrays);
    B = cmsAllocSampledCurve(nGrays);
    L = cmsAllocSampledCurve(nGrays);
            
	nGrays = 0;
	    
    /* Collect patches    */
    for (i=0; i < m -> nPatches; i++) {

                if (Neutrals[i]) {

                        LPPATCH gr = m -> Patches + i;

						
                        R -> Values[nGrays] = gr -> Colorant.RGB[0];
                        G -> Values[nGrays] = gr -> Colorant.RGB[1];
                        B -> Values[nGrays] = gr -> Colorant.RGB[2];
						L -> Values[nGrays] = gr -> XYZ.Y; 
                        
                        nGrays++;
                }

    }


	gr = BuildGammaTable(R, L, nResultingPoints);
	gg = BuildGammaTable(G, L, nResultingPoints);
	gb = BuildGammaTable(B, L, nResultingPoints);

	cmsFreeSampledCurve(R);
	cmsFreeSampledCurve(G);
	cmsFreeSampledCurve(B);
	cmsFreeSampledCurve(L);

	if (ColorSpace == PT_Lab) {

		LPGAMMATABLE Gamma3 = cmsBuildGamma(nResultingPoints, 3.0);

		Lin[0] = cmsJoinGammaEx(gr, Gamma3, nResultingPoints);
		Lin[1] = cmsJoinGammaEx(gg, Gamma3, nResultingPoints);
		Lin[2] = cmsJoinGammaEx(gb, Gamma3, nResultingPoints);

		cmsFreeGamma(gr); cmsFreeGamma(gg); cmsFreeGamma(gb);
		cmsFreeGamma(Gamma3);
	}
	else {


		LPGAMMATABLE Gamma1 = cmsBuildGamma(nResultingPoints, 1.0);

		Lin[0] = cmsJoinGammaEx(gr, Gamma1, nResultingPoints);
		Lin[1] = cmsJoinGammaEx(gg, Gamma1, nResultingPoints);
		Lin[2] = cmsJoinGammaEx(gb, Gamma1, nResultingPoints);

		cmsFreeGamma(gr); cmsFreeGamma(gg); cmsFreeGamma(gb);
		cmsFreeGamma(Gamma1);
		
	}

}



/* Apply linearization. WORD encoded version */

void cmsxApplyLinearizationGamma(WORD In[3], LPGAMMATABLE Gamma[3], WORD Out[3])
{
        L16PARAMS Lut16;

        cmsCalcL16Params(Gamma[0] -> nEntries, &Lut16);

        Out[0] = cmsLinearInterpLUT16(In[0], Gamma[0] -> GammaTable, &Lut16);
        Out[1] = cmsLinearInterpLUT16(In[1], Gamma[1] -> GammaTable, &Lut16);
        Out[2] = cmsLinearInterpLUT16(In[2], Gamma[2] -> GammaTable, &Lut16);


}



/* Apply linearization. double version */

void cmsxApplyLinearizationTable(double In[3], LPGAMMATABLE Gamma[3], double Out[3])
{
        WORD rw, gw, bw;
        double rd, gd, bd;
        L16PARAMS Lut16;


        cmsCalcL16Params(Gamma[0] -> nEntries, &Lut16);

		rw = (WORD) floor(_cmsxSaturate255To65535(In[0]) + .5);
		gw = (WORD) floor(_cmsxSaturate255To65535(In[1]) + .5);
		bw = (WORD) floor(_cmsxSaturate255To65535(In[2]) + .5);

        rd = cmsLinearInterpLUT16(rw , Gamma[0] -> GammaTable, &Lut16);
        gd = cmsLinearInterpLUT16(gw,  Gamma[1] -> GammaTable, &Lut16);
        bd = cmsLinearInterpLUT16(bw,  Gamma[2] -> GammaTable, &Lut16);

        Out[0] = _cmsxSaturate65535To255(rd);            /* back to 0..255 */
        Out[1] = _cmsxSaturate65535To255(gd);
        Out[2] = _cmsxSaturate65535To255(bd);
}

