1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 package info.magnolia.imaging.util;
35
36 import java.awt.Color;
37 import java.lang.reflect.Field;
38 import java.lang.reflect.Modifier;
39 import java.util.Map;
40 import java.util.TreeMap;
41 import java.util.regex.Matcher;
42 import java.util.regex.Pattern;
43
44 import org.apache.commons.lang3.StringUtils;
45
46
47
48
49
50
51 public class ColorConverter {
52 private static final Map<String, Color> namedColors = reflectivelyGetNamedColors();
53 private static final Pattern HEX = Pattern.compile("^#[0-9A-Fa-f]{3,6}$");
54 private static final Pattern FUNC = Pattern.compile("^(rgb|rgba|hsl|hsla)\\(" +
55 "([0-9]{1,3})" +
56 ",([0-9]{1,3}%?)" +
57 ",([0-9]{1,3}%?)" +
58 "(?:,([0-9.]{1,4}))?" +
59 "\\)$");
60
61 public static Color toColor(final String input) {
62 if (input == null) {
63 throw cantParse(input, "please provide either an #ffffff or #fff hexadecimal value or a known named color.");
64 }
65 final String code = StringUtils.deleteWhitespace(input).toLowerCase();
66 if (namedColors.containsKey(code)) {
67 return namedColors.get(code);
68 } else if (HEX.matcher(code).matches()) {
69 if (StringUtils.length(code) == 4) {
70
71 return Color.decode("#" + code.charAt(1) + code.charAt(1) + code.charAt(2) + code.charAt(2) + code.charAt(3) + code.charAt(3));
72 } else if (StringUtils.length(code) == 7) {
73 return Color.decode(code);
74 } else {
75 throw cantParse(input, "please provide a 6 or 3 digit long hexadecimal string, e.g #ffffff or #fff.");
76 }
77
78 } else {
79 final Matcher matcher = FUNC.matcher(code);
80 if (matcher.matches()) {
81 final String function = matcher.group(1);
82
83
84 final String alphaStr = matcher.group(5);
85 final int alpha;
86 if (StringUtils.endsWith(function, "a")) {
87 alpha = (int) (rangedFloat(alphaStr, input, 0, 1) * 255);
88 } else if (alphaStr != null) {
89 throw cantParse(input, "the " + function + "() function does not take an <alphavalue> argument.");
90 } else {
91 alpha = 255;
92 }
93
94 switch (function) {
95 case "rgb":
96 return new Color(
97 rangedInt(0, 255, matcher, 2, input),
98 rangedInt(0, 255, matcher, 3, input),
99 rangedInt(0, 255, matcher, 4, input)
100 );
101 case "rgba":
102 return new Color(
103 rangedInt(0, 255, matcher, 2, input),
104 rangedInt(0, 255, matcher, 3, input),
105 rangedInt(0, 255, matcher, 4, input),
106 alpha
107 );
108 case "hsl":
109 return new Color(hsbToRgb(input, matcher));
110 case "hsla":
111 int rgb = hsbToRgb(input, matcher);
112 int rgba = (alpha & 0xFF) << 24 | rgb;
113 final Color color = new Color(rgba);
114 return new Color(color.getRed(), color.getGreen(), color.getBlue(), alpha);
115 default:
116 throw cantParse(input, "'" + function + "()' is not a valid function. Please use rgb(), rgba(), hsl(), hsla(), a #ffffff hexadecimal value or a known named color.");
117 }
118 }
119 }
120 throw cantParse(input, "please provide either an #ffffff or #fff hexadecimal value or a known named color.");
121 }
122
123 private static int hsbToRgb(String input, Matcher matcher) {
124 final float[] hsb = hslToHsv(
125 normalizeAngle(Integer.parseInt(matcher.group(2))),
126 percentage(matcher.group(3), input),
127 percentage(matcher.group(4), input));
128 return Color.HSBtoRGB(hsb[0], hsb[1], hsb[2]);
129 }
130
131
132
133
134
135
136
137 private static float[] convertHslToHsb(float hslH, float hslS, float hslL) {
138 final float h = hslH / 360;
139 final float s = ((float) hslS / 100);
140 final float l = ((float) hslL / 100);
141 final float hsbB =
142 (
143 (2 * l) + (hslS * (1 - Math.abs((2 * l) - 1)))
144 )
145 / 2;
146 final float hsbS = (2 * (hsbB - l)) / hsbB;
147
148 return new float[] { h, hsbS, hsbB };
149 }
150
151
152
153
154
155
156
157
158
159 static float[] hslToHsv(int hslH, int hslS, int hslL) {
160 final float h = (float) hslH / 360;
161 final float s = ((float) hslS / 100);
162 final float l = ((float) hslL / 100);
163
164 float saturation = 0.0f;
165 float value = 0.0f;
166 if (l > 0) {
167 float lumScale = 1.0f - Math.max(l - 0.5f, 0f) * 2f;
168 lumScale = (lumScale == 0f) ? 0f : 1.0f / lumScale;
169 float lumStart = Math.max(0f, lumScale - 0.5f);
170 float lumDiv = (lumScale - lumStart);
171 lumDiv = (lumStart + (s * lumDiv));
172 saturation = (lumDiv == 0) ? 0 : (s / lumDiv);
173 value = l + (1.0f - l) * s;
174 }
175 return new float[] { h, saturation, value };
176 }
177
178 private static int rangedInt(int min, int max, Matcher matcher, int groupNr, String wholeInputForReporting) {
179 final String value = matcher.group(groupNr);
180 return rangedInt(min, max, value, wholeInputForReporting);
181 }
182
183 private static int rangedInt(int min, int max, String value, String wholeInputForReporting) {
184 if (StringUtils.endsWith(value, "%")) {
185 throw cantParse(wholeInputForReporting, value + " should not be a percentage value.");
186 }
187 final int i = Integer.parseInt(value);
188 if (i < min || i > max) {
189 throw cantParse(wholeInputForReporting, "%s is not in the allowed %d-%d range.", value, min, max);
190 }
191 return i;
192 }
193
194 private static float rangedFloat(String value, String wholeInputForReporting, float min, float max) {
195 final float f = Float.parseFloat(value);
196 if (f < min || f > max) {
197 throw cantParse(wholeInputForReporting, "%s is not in the allowed %.1f-%.1f <alphavalue> range.", value, min, max);
198 }
199 return f;
200 }
201
202 static int percentage(String value, String wholeInputForReporting) {
203 if (!StringUtils.endsWith(value, "%")) {
204 throw cantParse(wholeInputForReporting, value + " should be a percentage value.");
205 }
206 return rangedInt(0, 100, value.substring(0, value.length() - 1), wholeInputForReporting);
207 }
208
209 static int normalizeAngle(int angle) {
210 final int modulo = angle % 360;
211 if (modulo < 0) {
212 return modulo + 360;
213 } else {
214 return modulo;
215 }
216 }
217
218
219
220
221
222
223
224
225
226
227
228 private static Map<String, Color> reflectivelyGetNamedColors() {
229 try {
230 final Map<String, Color> namedColors = new TreeMap<String, Color>(String.CASE_INSENSITIVE_ORDER);
231 final Field[] fields = Color.class.getFields();
232 for (final Field field : fields) {
233 int mod = field.getModifiers();
234 if (Modifier.isStatic(mod) && Modifier.isPublic(mod) && Modifier.isFinal(mod)) {
235 if (Color.class.equals(field.getType())) {
236 namedColors.put(field.getName().toLowerCase(), (Color) field.get(null));
237 }
238 }
239 }
240 return namedColors;
241 } catch (IllegalAccessException e) {
242 throw new RuntimeException("Can't access field values of java.awt.Color, is this system too secure?", e);
243 }
244 }
245
246 private static RuntimeException cantParse(String input, String messageFmt, Object... args) {
247 return new IllegalArgumentException("Can't decode color " + input + ": " + String.format(messageFmt, args));
248 }
249
250 }