package au.net.causal.projo.prefs.transform;
import java.awt.Color;
import java.awt.Transparency;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import au.net.causal.projo.prefs.PreferencesException;
/**
* Converts between {@link Color} values and strings.
* <p>
*
* The string representation of a color closely resembles that which is used in CSS definitions.
* The following syntaxes are supported:
*
* <ul>
* <li>
* {@literal #}(hex): e.g. <code>#FF0000</code></li>. Alpha is not supported in this representation, and is assumed to be fully opaque. Both the
* single-digit and double-digit per component format is allowed.
* </li>
* <li>
* rgb() and rgba() functions: Each component can be either an integer 0-255, a numeric percentage with trailing '%' sign, or
* a floating point number (with mandatory decimal point) between 0.0 and 1.0.
* </li>
* <li>
* hsl() or hsla() functions: Components are defined the same as rgb() and rgba() functions.
* </li>
* </ul>
* <p>
*
* For converting to strings, the following rules apply:
* <ul>
* <li>The double-digit hex form is used if the color is fully opaque</li>
* <li>Otherwise, the rgba() function is used with floating point components</li>
* </ul>
*
* @author prunge
*/
public class ColorToStringTransformer extends GenericToStringTransformer<Color>
{
private static final Logger log = LoggerFactory.getLogger(ColorToStringTransformer.class);
public ColorToStringTransformer()
{
super(Color.class, true);
}
private float[] interpretColorFunction(String s, String functionName, int numArgs)
{
String expectedStart = functionName + "(";
if (!s.startsWith(expectedStart))
return(null);
int endBracketIndex = s.indexOf(')');
if (endBracketIndex < 0)
return(null);
String body = s.substring(expectedStart.length(), endBracketIndex);
String[] sArgs = body.split(Pattern.quote(","));
if (sArgs.length != numArgs)
return(null);
//Interpret each arg
//Possible forms:
// - int value between 0 and 255 (clamped)
// - numeric percentage value with trailing '%' sign
// - floating point value between 0.0 and 1.0 (clamped)
float[] args = new float[numArgs];
for (int i = 0; i < numArgs; i++)
{
String sArg = sArgs[i];
sArg = sArg.trim();
//Percentage
if (sArg.endsWith("%"))
{
String sNum = sArg.substring(0, sArg.length() - 1);
sNum = sNum.trim();
try
{
float percent = Float.parseFloat(sNum);
args[i] = percent / 100.0f;
}
catch (NumberFormatException e)
{
log.debug("Failed to parse number " + sNum + " in " + s + ".", e);
return(null);
}
}
//Floating point number
else if (sArg.contains("."))
{
try
{
args[i] = Float.parseFloat(sArg);
}
catch (NumberFormatException e)
{
log.debug("Failed to parse number " + sArg + " in " + s + ".", e);
return(null);
}
}
else
{
//Integer number
try
{
args[i] = Integer.parseInt(sArg) / 255.0f;
}
catch (NumberFormatException e)
{
log.debug("Failed to parse number " + sArg + " in " + s + ".", e);
return(null);
}
}
}
return(args);
}
@Override
protected Color stringToValue(String s) throws PreferencesException
{
if (StringUtils.isEmpty(s))
return(null);
s = s.trim();
//Formats supported:
//rgba(r, g, b, a)
float[] args = interpretColorFunction(s, "rgba", 4);
if (args != null)
return(new Color(args[0], args[1], args[2], args[3]));
//rgb(r, g, b)
args = interpretColorFunction(s, "rgb", 3);
if (args != null)
return(new Color(args[0], args[1], args[2]));
//hsla(h, s, l, a)
args = interpretColorFunction(s, "hsla", 4);
if (args != null)
{
Color hsbColor = Color.getHSBColor(args[0], args[1], args[2]);
return(new Color(hsbColor.getRed(), hsbColor.getGreen(), hsbColor.getBlue(), (int)(args[3] * 255)));
}
//hsl(h, s, l)
args = interpretColorFunction(s, "hsl", 3);
if (args != null)
return(Color.getHSBColor(args[0], args[1], args[2]));
//hex form with preceding #
String hexForm = s;
if (hexForm.startsWith("#"))
hexForm = hexForm.substring(1);
//Single-digit hex representation, convert to double digit form
if (hexForm.length() == 3)
{
hexForm = StringUtils.repeat(hexForm.charAt(0), 2) +
StringUtils.repeat(hexForm.charAt(1), 2) +
StringUtils.repeat(hexForm.charAt(2), 2);
}
//Double-digit hex representation
try
{
int intForm = Integer.parseInt(hexForm, 16);
return(new Color(intForm));
}
catch (NumberFormatException e)
{
log.debug("Not in hex form: " + s, e);
}
//If we get here we could not interpret as color
throw new PreferencesException("Failed to parse '" + s + "' as a Color.");
}
@Override
protected String valueToString(Color value) throws PreferencesException
{
if (value == null)
return(null);
//If fully opaque (no alpha translucency) then write out in hex form
if (isOpaque(value))
{
return("#" + StringUtils.leftPad(Integer.toHexString(value.getRed()), 2, "0") +
StringUtils.leftPad(Integer.toHexString(value.getGreen()), 2, "0") +
StringUtils.leftPad(Integer.toHexString(value.getBlue()), 2, "0"));
}
//Otherwise use rgba representation
float[] components = value.getRGBComponents(null);
return("rgba(" + components[0] + ", " + components[1] + ", " + components[2] + ", " + components[3] + ")");
}
private static boolean isOpaque(Color color)
{
return(color.getTransparency() == Transparency.OPAQUE);
}
}