/**
*** NTC.java, v1.20, 2010-12-10
*** This class is a rewrite of Chirag Mehta's "Name That Color" javascript
*** (ntc.js) library.  See:  http://chir.ag/projects/ntc
***	Rich Franzen	http://r0k.us/rock/
***
*** cvs: $ID$
**/
/*

Latest source at: http://r0k.us/source/NTC.java

This source is released under the: Creative Commons License:
Attribution 2.5 --  http://creativecommons.org/licenses/by/2.5/

Sample Usage as standalone:

	type: java NTC 28e7b3
	out:  #30D5C8, Turquoise, false

For use in a larger program/applet, call NTC.init() once

*/
package us.r0k.ntc;

import java.io.InputStream;
import java.io.IOException;
import java.io.FileNotFoundException;
import java.util.*;
import java.awt.Color;

public class NTC
{
    private static float[][] labhsb;	// size of names[names.length][h]
    private static float distance;	// min distance on last search

    public final static String defaultCND = new String("/cnd_ntc.properties");
    private static String cndNameFile[] = new String[2];
    private static boolean verbose = false;

    public static void init()
    {
	String	color;
	int	rgb[] = {0,0,0};
	double  xyz[] = {0,0,0};
	double  lab[] = {0,0,0};
	float	hsb[] = {0,0,0};
	int	i;

	if (verbose)
	    System.out.println("init() for " + cndNameFile[0]);

	if (cndNameFile[0] == null)
	    readCND(null);  // read default dictionary

	labhsb = new float [names.length][6];
	for (i=0; i<names.length; i++)
	{
	    color = names[i][0];
	    getRGB(color, rgb);
	    // L is between 0 and 100, while a and b are between -110 and 110
	    RGBtoXYZ(rgb[0], rgb[1], rgb[2], xyz);
	    XYZtoCIElab(xyz[0], xyz[1], xyz[2], lab);
	    labhsb[i][0] = (float)lab[0];
	    labhsb[i][1] = (float)lab[1];
	    labhsb[i][2] = (float)lab[2];
	    Color.RGBtoHSB(rgb[0], rgb[1], rgb[2], hsb);
	    labhsb[i][3] = hsb[0];
	    labhsb[i][4] = hsb[1];
	    labhsb[i][5] = hsb[2];
	}
    }

    // valid input is hex string of form rrggbb or #rrggbb
    // outputs hex value of found color and color name
    // return value is true only on exact match
    public static boolean name(String hexIn, String h_n[])
    {
	String myHex = hexIn;

	int	len, i;
	char	temp[] = {'0', '0', '0', '0', '0', '0' };
	int	rgb[] = {0, 0, 0};
	double  xyz[] = {0,0,0};
	double  lab[] = {0,0,0};
	float	L, a, b;
	float	hsb[] = {0,0,0};
	float	diff;
	float	dLab = 0;
	float	dHSB = 0;

	float	df = 999999;	// > largest possible diff
	int	cl = -1;	// illegal index

	// ready some defaults
	h_n[0] = "#000000";
	h_n[1] = "Invalid Color: ";
	boolean status = false;

	// preliminary validation
	if ((hexIn == null) || (hexIn == ""))
	    return(status);

	h_n[1] = "Invalid Color: " + hexIn;  // now know its safe to append

	// remove possible leading # (technically, removes all #)
	if (hexIn.charAt(0) == '#')  myHex = hexIn.replace("#", "");
	// final validation
	if (!isHex(myHex))  return(status);

	len = myHex.length();
	if (len == 3)
	{   // support CSS 3-hex-digit color: abc --> aabbcc
	    for (i=0; i<3; i++)
	    {
		char twin = myHex.charAt(i);
	    	temp[2*i] = twin;
	    	temp[2*i+1] = twin;
	    }
	    myHex = new String(temp);
	}
	  else if (len < 6)
	  { // prepend 0's
	    for (i=len-1; i>=0; i--)
	    	temp[6-len+i] = myHex.charAt(i);
	    myHex = new String(temp);
	  }
	  // else if len>6, ignore trailing characters

	myHex = myHex.toUpperCase();

	getRGB(myHex, rgb);
	RGBtoXYZ(rgb[0], rgb[1], rgb[2], xyz);
	XYZtoCIElab(xyz[0], xyz[1], xyz[2], lab);
	L = (float)lab[0];
	a = (float)lab[1];
	b = (float)lab[2];
	Color.RGBtoHSB(rgb[0], rgb[1], rgb[2], hsb);
	if (verbose) {
	    System.out.println("r=" + rgb[0] +
			       ", g=" + rgb[1] + ", b=" + rgb[2]);
	    System.out.println("x=" + xyz[0] +
			       ", y=" + xyz[1] + ", z=" + xyz[2]);
	    System.out.println("L=" + lab[0] +
			       ", a=" + lab[1] + ", b=" + lab[2]);
	    System.out.println("H=" + hsb[0] +
			       ", S=" + hsb[1] + ", B=" + hsb[2]);
	}

	for (i = 0; i < names.length; i++)
	{
	    if (myHex.equals(names[i][0]))
	    {
		cl = i;
		status = true;
		break;	// exact match; we're done
	    }

	    //    0 <= L <= 100, maxDelta = 100
	    // -110 <= a <= 110, maxDelta = 220
	    // -110 <= b <= 110, maxDelta = 220
	    //    0 <= H <  1,   maxDelta ~ 1
	    //    0 <= S <= 1,   maxDelta = 1
	    //    0 <= B <= 1,   maxDelta = 1
	    diff  = L - labhsb[i][0];
	    dLab  = diff * diff;
	    diff  = a - labhsb[i][1];
	    dLab += diff * diff;
	    diff  = b - labhsb[i][2];
	    dLab += diff * diff;
	    dLab  = (float)Math.pow(dLab, 0.5);
	    diff  = Math.abs(hsb[0] - labhsb[i][3]);
	    if (diff > 0.5)  diff = 1.0f - diff;  // correct for hue rollover
	    diff *= 150;	// between maxDelta of L and a,b
	    dHSB  = diff * diff;
	    diff  = (hsb[1] - labhsb[i][4]) * 150;
	    dHSB += diff * diff;
	    diff  = (hsb[2] - labhsb[i][5]) * 100;  // match B maxDelta to L's
	    dHSB += diff * diff;
	    dHSB  = (float)Math.pow(dHSB, 0.5);
	    diff  = dLab + dHSB;

	    if (df > diff)
	    {	// found closer match
		df = diff;
		cl = i;
	    }
	}

	if (cl >= 0)
	{
	    h_n[0] = "#" + names[cl][0];
	    h_n[1] = names[cl][1];
	    if (status)
		distance = 0;
	      else
		distance = df;
	}

	return(status);
    }


    public static void main(String args[])
    {
	if (args.length == 0)
	{
	    System.out.println("usage: java NTC hex [filename]");
	    System.out.println("ex. 1: java NTC 8a67b2");
	    System.out.println("ex. 2: java NTC 1c2d3e cnd_w3c.properties");
	    System.exit(-1);
	}
	String	color = args[0];
	String	hex_name[] = {"zippity", "doodah"};
	String	cnd = null;
	boolean	exact;

	verbose = true;
	if (args.length > 1)  cnd = args[1];
	if (!readCND(cnd))  init();
	exact = name(color, hex_name);
	System.out.println(hex_name[0] + ", " + hex_name[1] + ", " +
	    Boolean.toString(exact));
    }

    // return name of current Color Name Dictionary
    final public static String getCNDname() {
	return(cndNameFile[0]);
    }
    // return file of current Color Name Dictionary
    final public static String getCNDfile() {
	return(cndNameFile[1]);
    }
    // return minimum distance in last search
    final public static float getDistance() {
	return(distance);
    }

    // input color hex string of form "rrggbb"
    // output array is the rgb components within the string
    final public static void getRGB(String color, int rgb[])
    {
	int i = 0;
	if (color.charAt(0) == '#')  i++;
	rgb[0] = Integer.valueOf(color.substring(i,i+2), 16).intValue();
	i += 2;
	rgb[1] = Integer.valueOf(color.substring(i,i+2), 16).intValue();
	i += 2;
	rgb[2] = Integer.valueOf(color.substring(i,i+2), 16).intValue();
    }

    // found at: http://codingforums.com/showthread.php?t=68429
    public static boolean isHex(String in)
    {
        boolean ret;
        try {
            // try to parse the string to an integer, using 16 as radix
            int t = Integer.parseInt(in, 16);
            // parsing succeeded, string is valid hex number
            ret = true;
        } catch (NumberFormatException e) {
            // parsing failed, string is not a valid hex number
            ret = false;
        }
        return(ret);
    }


    // encapsulate opening of a properties file.  returns null if problem
    public static Properties loadProperties(String file)
    {
	String me = new String("loadProperties(): ");
	if (file == null) {
	    System.out.println(me + "null filename");
	    return(null);
	}

	Properties p = new Properties();
	InputStream fis;

	fis = NTC.class.getResourceAsStream(file);
	if (fis == null) {
	    System.out.println(me + "cannot access " + file);
	    return(null);
	}

	try {
	    p.load(fis);
	} catch (IOException e) {
	    System.out.println(me + "cannot load " + file);
	    try {
		fis.close();
	    } catch (IOException e2) { /* what the heck you gonna do? */ }
	    return(null);
	}
	try {
	    fis.close();
	} catch (IOException e) { /* what the heck you gonna do? */ }

	return(p);
    }

    // read in a properties file containing a color name dictionary (CND)
    public static boolean readCND(String cnd)
    {
	String me = new String("readCND(): ");
	if (cnd == null)  cnd = defaultCND;
	Properties props = loadProperties(cnd);
	if (props == null)  return(false);

	ArrayList<String> sorter = new ArrayList<String>(1000);
	String key;
	Enumeration hexKeys = props.propertyNames();
	while (hexKeys.hasMoreElements()) {
	    key = (String)hexKeys.nextElement();
	    if (isHex(key)) {
		if (key.length() == 6)  sorter.add(key);
	    }
	      else if (verbose)
	        System.out.println(me + "special key, " + key);
	}
	int count = sorter.size();
	if (count < 1) {
	    System.out.println(me + "no hex values in " + cnd);
	    return(false);
	}

	// keys were in hashtable order.  sort them
	Collections.sort(sorter);
	names = new String[count][2];

	String name;
	for (int i = 0; i < count; i++) {
	    name = props.getProperty(sorter.get(i), "no name");
	    names[i][0] = sorter.get(i);
	    names[i][1] = name;
	}

	cndNameFile[0] = props.getProperty("CND.name", "unknown");
	cndNameFile[1] = cnd;
	init();		// update comparison data (after setting cnd_name)
	return (true);
    }


    // algorithm from http://easyrgb.com/index.php?X=MATH
    public static void RGBtoXYZ(int r, int g, int b, double xyz[])
    {
	// input rgb ranged from 0 to 255, inclusive
	double red = r / 255.0;
	double grn = g / 255.0;
	double blu = b / 255.0;
	if (xyz == null)  xyz = new double[3];

	if (red > .04045)
	    red = Math.pow((red + 0.055) / 1.055, 2.4);
	  else
	    red = red / 12.92;
	if (grn > .04045)
	    grn = Math.pow((grn + 0.055) / 1.055, 2.4);
	  else
	    grn = grn / 12.92;
	if (blu > .04045)
	    blu = Math.pow((blu + 0.055) / 1.055, 2.4);
	  else
	    blu = blu / 12.92;

	red *= 100.0;
	grn *= 100.0;
	blu *= 100.0;

	//Observer. = 2°, Illuminant = D65
	xyz[0] = red * 0.4124 + grn * 0.3576 + blu * 0.1805;
	xyz[1] = red * 0.2126 + grn * 0.7152 + blu * 0.0722;
	xyz[2] = red * 0.0193 + grn * 0.1192 + blu * 0.9505;
    }


    // algorithm from http://easyrgb.com/index.php?X=MATH
    public static void XYZtoCIElab(double x, double y, double z, double lab[])
    {
	double xx = x /  95.047;	// Observer= 2°, Illuminant= D65
	double yy = y / 100.000;
	double zz = z / 108.883;
	if (lab == null)  lab = new double[3];

	if (xx > 0.008856)
	    xx = Math.pow(xx, 1.0/3.0);
	  else
	    xx = (7.787 * xx) + (16.0 / 116.0);
	if (yy > 0.008856)
	    yy = Math.pow(yy, 1.0/3.0);
	  else
	    yy = (7.787 * yy) + (16.0 / 116.0);
	if (zz > 0.008856)
	    zz = Math.pow(zz, 1.0/3.0);
	  else
	    zz = (7.787 * zz) + (16.0 / 116.0);

	lab[0] = (116 * yy) - 16;
	lab[1] = 500 * (xx - yy);
	lab[2] = 200 * (yy - zz);
    }

    public static String names[][] = {
	{"000000", "Black"},
	{"0000FF", "Blue"},
	{"00FF00", "Green"},
	{"00FFFF", "Cyan"},
	{"FF0000", "Red"},
	{"FF00FF", "Magenta"},
	{"FFFF00", "Yellow"},
	{"FFFFFF", "White"}
    };
}

