/*
* Copyright 2013-2015 Skynav, Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY SKYNAV, INC. AND ITS CONTRIBUTORS “AS IS” AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL SKYNAV, INC. OR ITS CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.skynav.ttv.verifier.util;
import java.util.Collections;
import java.util.Map;
import org.xml.sax.Locator;
import com.skynav.ttv.model.value.Color;
import com.skynav.ttv.model.value.impl.ColorImpl;
import com.skynav.ttv.util.Location;
import com.skynav.ttv.util.Message;
import com.skynav.ttv.util.Reporter;
import com.skynav.ttv.verifier.VerifierContext;
public class Colors {
public static boolean maybeColor(String value) {
value = value.trim();
if (value.length() == 0)
return false;
else if (value.charAt(0) == '#')
return true;
else if (value.indexOf("rgb") == 0)
return true;
else if (Strings.isLetters(value))
return true;
else
return false;
}
public static boolean isColor(String value, Location location, VerifierContext context, Color[] outputColor) {
if (isRGBHash(value, outputColor))
return true;
else if (isRGBFunction(value, outputColor))
return true;
else if (isNamedColor(value, outputColor))
return true;
else
return false;
}
public static void badColor(String value, Location location, VerifierContext context) {
if (value.charAt(0) == '#')
badRGBHash(value, location, context);
else if (value.indexOf("rgb") == 0)
badRGBFunction(value, location, context);
else if (Strings.isLetters(value))
badNamedColor(value, location, context);
else {
Reporter reporter = context.getReporter();
reporter.logInfo(reporter.message(location.getLocator(), "*KEY*",
"Bad <color> expression, got ''{0}'', but expected <#rrggbb>, #<rrggbbaa>, <rgb(...)>, <rgba(...)>, or <named color>.", value));
}
}
private static final Map<String,Color> namedColors;
private static final double cc80 = 128.0 / 255.0;
private static final double ccC0 = 192.0 / 255.0;
private static final Object[][] namedColorValues = new Object[][] {
{ "transparent", new ColorImpl(0,0,0,0) },
{ "black", new ColorImpl(0,0,0,1) },
{ "silver", new ColorImpl(ccC0,ccC0,ccC0,1) },
{ "gray", new ColorImpl(cc80,cc80,cc80,1) },
{ "white", new ColorImpl(1,1,1,1) },
{ "maroon", new ColorImpl(cc80,0,0,1) },
{ "red", new ColorImpl(1,0,0,1) },
{ "purple", new ColorImpl(cc80,0,cc80,1) },
{ "fuchsia", new ColorImpl(1,0,1,1) },
{ "magenta", new ColorImpl(1,0,1,1) },
{ "green", new ColorImpl(0,cc80,0,1) },
{ "lime", new ColorImpl(0,1,0,1) },
{ "olive", new ColorImpl(cc80,cc80,0,1) },
{ "yellow", new ColorImpl(1,1,0,1) },
{ "navy", new ColorImpl(0,0,cc80,1) },
{ "blue", new ColorImpl(0,0,1,1) },
{ "teal", new ColorImpl(0,cc80,cc80,1) },
{ "aqua", new ColorImpl(0,1,1,1) },
{ "cyan", new ColorImpl(0,1,1,1) },
};
static {
Map<String,Color> m = new java.util.HashMap<String,Color>();
for (Object[] namedColor : namedColorValues) {
m.put((String) namedColor[0], (Color) namedColor[1]);
}
namedColors = Collections.unmodifiableMap(m);
}
private static boolean isNamedColor(String value, Color[] outputColor) {
if (namedColors.containsKey(value)) {
if (outputColor != null)
outputColor[0] = namedColors.get(value);
return true;
} else
return false;
}
private static void badNamedColor(String value, Location location, VerifierContext context) {
assert Strings.isLetters(value);
Reporter reporter = context.getReporter();
reporter.logInfo(reporter.message(location.getLocator(), "*KEY*", "Unknown named color, got ''{0}''.", value));
}
private static boolean isRGBComponent(String value, int minValue, int maxValue, Double[] outputValue) {
try {
int componentValue = Integer.parseInt(value);
if ((componentValue < minValue) || (componentValue > maxValue))
return false;
else {
if (outputValue != null)
outputValue[0] = Double.valueOf((double) componentValue / 255.0);
return true;
}
} catch (NumberFormatException e) {
return false;
}
}
private static void badRGBComponent(String value, Location location, VerifierContext context, int minValue, int maxValue) {
Reporter reporter = context.getReporter();
Locator locator = location.getLocator();
try {
int componentValue = Integer.parseInt(value);
if ((componentValue < minValue) || (componentValue > maxValue) ) {
reporter.logInfo(reporter.message(locator, "*KEY*",
"Component out of range [{0},{1}] in <rgb(...)> or <rgba(...)> color expression, got {2}.",
minValue, maxValue, componentValue));
}
} catch (NumberFormatException e) {
if (Strings.containsXMLSpace(value)) {
String trimmedComponent = value.trim();
try {
Integer.parseInt(trimmedComponent);
reporter.logInfo(reporter.message(locator, "*KEY*",
"XML space padding not permitted in <rgb(...)> or <rgba(...)> color expression component, got ''{0}''.", value));
} catch (NumberFormatException ee) {
reporter.logInfo(reporter.message(locator, "*KEY*",
"Component in <rgb(...)> or <rgba(...)> color expression is not a non-negative integer, got ''{0}''.", value));
}
} else {
reporter.logInfo(reporter.message(locator, "*KEY*",
"Component in <rgb(...)> or <rgba(...)> color expression is not a non-negative integer, got ''{0}''.", value));
}
}
}
private static boolean isRGBComponents(String components, int[][] valueLimits, Color[] outputColor) {
String[] colorComponents = components.split(",");
int numComponents = colorComponents.length;
if (numComponents != valueLimits.length)
return false;
double r = 0;
double g = 0;
double b = 0;
double a = 1;
int componentIndex = 0;
for (String component : colorComponents) {
int[] limits = valueLimits[componentIndex++];
int minValue = limits[0];
int maxValue = limits[1];
Double[] outputValue = new Double[1];
if (!isRGBComponent(component, minValue, maxValue, outputValue))
return false;
if (componentIndex == 0)
r = outputValue[0].doubleValue();
if (componentIndex == 1)
g = outputValue[0].doubleValue();
if (componentIndex == 2)
b = outputValue[0].doubleValue();
if (componentIndex == 3)
a = outputValue[0].doubleValue();
}
if (outputColor != null)
outputColor[0] = new ColorImpl(r, g, b, a);
return true;
}
private static void badRGBComponents(String components, Location location, VerifierContext context, int[][] valueLimits) {
String[] colorComponents = components.split(",");
int numComponents = valueLimits.length;
if (colorComponents.length != numComponents) {
Reporter reporter = context.getReporter();
Locator locator = location.getLocator();
Message message;
if (colorComponents.length < numComponents) {
message = reporter.message(locator, "*KEY*",
"Missing component in <rgb(...)> or <rgba(...)> color expression, got {0}, expected {1} components.",
colorComponents.length, numComponents);
} else {
message = reporter.message(locator, "*KEY*",
"Extra component in <rgb(...)> or <rgba(...)> color expression, got {0}, expected {1} components.",
colorComponents.length, numComponents);
}
reporter.logInfo(message);
}
int componentIndex = 0;
for (String component : colorComponents) {
// if extra component, then use last limits
int[] limits = (componentIndex < numComponents) ? valueLimits[componentIndex++] : valueLimits[numComponents - 1];
int minValue = limits[0];
int maxValue = limits[1];
if (!isRGBComponent(component, minValue, maxValue, null))
badRGBComponent(component, location, context, minValue, maxValue);
}
}
private static final int[][] rgbComponentLimits = new int[][] { { 0, 255 }, { 0, 255 }, { 0, 255 } };
private static final int[][] rgbaComponentLimits = new int[][] { { 0, 255 }, { 0, 255 }, { 0, 255 }, { 0, 255 } };
private static boolean isRGBFunction(String value, Color[] outputColor) {
int componentsStart;
int[][] valueLimits;
if (value.indexOf("rgb(") == 0) {
componentsStart = 4;
valueLimits = rgbComponentLimits;
} else if (value.indexOf("rgba(") == 0) {
componentsStart = 5;
valueLimits = rgbaComponentLimits;
} else
return false;
if (value.charAt(value.length() - 1) != ')')
return false;
return isRGBComponents(value.substring(componentsStart, value.length() - 1), valueLimits, outputColor);
}
private static void badRGBFunction(String value, Location location, VerifierContext context) {
Reporter reporter = context.getReporter();
Locator locator = location.getLocator();
assert value.indexOf("rgb") == 0;
int opIndex = value.indexOf("(");
if (opIndex < 0) {
reporter.logInfo(reporter.message(locator, "*KEY*",
"Bad RGB function syntax in <color> expression, got ''{0}'', missing opening parenthesis of argument list.", value));
} else {
String functionName = value.substring(0, opIndex);
if (!functionName.equals("rgb") && !functionName.equals("rgba")) {
reporter.logInfo(reporter.message(locator, "*KEY*",
"Bad RGB function syntax in <color> expression, got ''{0}'', incorrect function name, expect 'rgb' or 'rgba'.", value));
}
}
int cpIndex = value.indexOf(")");
if (cpIndex < 0) {
reporter.logInfo(reporter.message(locator, "*KEY*",
"Bad RGB function syntax in <color> expression, got ''{0}'', missing closing parenthesis of argument list.", value));
}
if ((opIndex >= 0) && (cpIndex >= 0)) {
int componentsStart = opIndex + 1;
int componentsEnd = cpIndex;
if (componentsStart <= componentsEnd) {
int[][] valueLimits;
if (value.indexOf("rgb(") == 0)
valueLimits = rgbComponentLimits;
else if (value.indexOf("rgba(") == 0)
valueLimits = rgbaComponentLimits;
else
valueLimits = null;
if (valueLimits != null)
badRGBComponents(value.substring(componentsStart, componentsEnd), location, context, valueLimits);
}
}
}
private static boolean isRGBHash(String value, Color[] outputColor) {
if (value.length() < 7)
return false;
else if (value.charAt(0) != '#')
return false;
else {
String digits = value.substring(1);
int numDigits = digits.length();
if ((numDigits != 6) && (numDigits != 8))
return false;
else if (!Strings.isHexDigits(digits))
return false;
else {
if (outputColor != null)
outputColor[0] = ColorImpl.fromRGBHash(digits);
return true;
}
}
}
private static void badRGBHash(String value, Location location, VerifierContext context) {
Reporter reporter = context.getReporter();
Locator locator = location.getLocator();
String hexDigits = null;
do {
int valueIndex = 0;
int valueLength = value.length();
char c;
// whitespace before hash sign
if (valueIndex == valueLength)
break;
c = value.charAt(valueIndex);
if (Characters.isXMLSpace(c)) {
while (Characters.isXMLSpace(c)) {
if (++valueIndex >= valueLength)
break;
c = value.charAt(valueIndex);
}
reporter.logInfo(reporter.message(locator, "*KEY*",
"Bad <#...> color expression, XML space padding not permitted before '#'."));
}
// hash sign
if (valueIndex == valueLength)
break;
c = value.charAt(valueIndex);
if (c != '#') {
reporter.logInfo(reporter.message(locator, "*KEY*",
"Bad <#...> color expression, expected '#', got '" + c + "'."));
break;
} else
valueIndex++;
// whitespace after hash sign
if (valueIndex == valueLength) {
reporter.logInfo(reporter.message(locator, "*KEY*",
"Bad <#...> color expression, no hexadecimal digits, expect 6 ('rrggbb') or 8 ('rrggbbaa') hexadecimal digits after '#'."));
break;
}
c = value.charAt(valueIndex);
if (Characters.isXMLSpace(c)) {
while (Characters.isXMLSpace(c)) {
if (++valueIndex >= valueLength)
break;
c = value.charAt(valueIndex);
}
reporter.logInfo(reporter.message(locator, "*KEY*",
"Bad <#...> color expression, XML space padding not permitted after '#'."));
}
// hex digits
if (valueIndex == valueLength) {
reporter.logInfo(reporter.message(locator, "*KEY*",
"Bad <#...> color expression, no hexadecimal digits, expect 6 ('rrggbb') or 8 ('rrggbbaa') hexadecimal digits after '#'."));
break;
}
c = value.charAt(valueIndex);
if (Characters.isHexDigit(c)) {
StringBuffer sb = new StringBuffer();
while (Characters.isHexDigit(c)) {
sb.append(c);
if (++valueIndex >= valueLength)
break;
c = value.charAt(valueIndex);
}
hexDigits = sb.toString();
}
// hex digits count
if (hexDigits != null) {
int numDigits = hexDigits.length();
if ((numDigits != 6) && (numDigits != 8)) {
reporter.logInfo(reporter.message(locator, "*KEY*",
"Bad <#...> color expression, expect 6 ('rrggbb') or 8 ('rrggbbaa') hexadecimal digits, got {0} digits.", numDigits));
}
} else {
reporter.logInfo(reporter.message(locator, "*KEY*",
"Bad <#...> color expression, no hexadecimal digits, expect 6 ('rrggbb') or 8 ('rrggbbaa') hexadecimal digits after '#'."));
}
// whitespace after hex digits
if (valueIndex == valueLength)
break;
c = value.charAt(valueIndex);
if (Characters.isXMLSpace(c)) {
while (Characters.isXMLSpace(c)) {
if (++valueIndex >= valueLength)
break;
c = value.charAt(valueIndex);
}
reporter.logInfo(reporter.message(locator, "*KEY*",
"Bad <#...> color expression, XML space padding not permitted after hexadecimal digits."));
}
// garbage after (hexDigit+ S*)
if (valueIndex < valueLength) {
StringBuffer sb = new StringBuffer();
while (valueIndex < valueLength) {
sb.append(value.charAt(valueIndex++));
}
Message message;
if (hexDigits == null) {
message = reporter.message(locator, "*KEY*",
"Bad <#...> color expression, unrecognized characters not permitted after sign, got ''{0}''.", sb.toString());
} else {
message = reporter.message(locator, "*KEY*",
"Bad <#...> color expression, unrecognized characters not permitted after hexadecimal digits, got ''{0}''.", sb.toString());
}
reporter.logInfo(message);
}
} while (false);
}
}