blob: d6a8934566b2ff159f656d455dcd4b5089014d9b [file] [log] [blame]
Lucas Dupin65f56582017-04-27 10:21:41 -07001/*
2 * Copyright (C) 2017 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License
15 */
16
Lucas Dupine2292a92017-07-06 14:35:30 -070017package com.android.internal.colorextraction.types;
Lucas Dupin65f56582017-04-27 10:21:41 -070018
Lucas Dupine2292a92017-07-06 14:35:30 -070019import android.annotation.NonNull;
20import android.annotation.Nullable;
Lucas Dupin65f56582017-04-27 10:21:41 -070021import android.app.WallpaperColors;
Lucas Dupin6e69c852017-07-06 16:09:24 -070022import android.content.Context;
Lucas Dupin65f56582017-04-27 10:21:41 -070023import android.graphics.Color;
Lucas Dupin65f56582017-04-27 10:21:41 -070024import android.util.Log;
25import android.util.MathUtils;
Lucas Dupin98ce4582017-05-22 19:43:45 -070026import android.util.Range;
Lucas Dupin65f56582017-04-27 10:21:41 -070027
Lucas Dupin6e69c852017-07-06 16:09:24 -070028import com.android.internal.R;
Lucas Dupine2292a92017-07-06 14:35:30 -070029import com.android.internal.annotations.VisibleForTesting;
30import com.android.internal.colorextraction.ColorExtractor.GradientColors;
31import com.android.internal.graphics.ColorUtils;
Lucas Dupin65f56582017-04-27 10:21:41 -070032
Lucas Dupin6e69c852017-07-06 16:09:24 -070033import org.xmlpull.v1.XmlPullParser;
34import org.xmlpull.v1.XmlPullParserException;
35
36import java.io.IOException;
37import java.util.ArrayList;
Lucas Dupin96a14c32017-06-22 15:35:15 -070038import java.util.Arrays;
Lucas Dupin84b89d92017-05-09 12:16:19 -070039import java.util.List;
40
Lucas Dupin65f56582017-04-27 10:21:41 -070041/**
42 * Implementation of tonal color extraction
43 */
44public class Tonal implements ExtractionType {
45 private static final String TAG = "Tonal";
46
47 // Used for tonal palette fitting
48 private static final float FIT_WEIGHT_H = 1.0f;
49 private static final float FIT_WEIGHT_S = 1.0f;
50 private static final float FIT_WEIGHT_L = 10.0f;
51
Lucas Dupin7aaa3532017-05-28 08:51:07 -070052 private static final boolean DEBUG = true;
Lucas Dupin65f56582017-04-27 10:21:41 -070053
Lucas Dupinc77b71d2017-07-05 17:34:41 -070054 public static final int MAIN_COLOR_LIGHT = 0xffe0e0e0;
Lucas Dupind26facc2018-09-10 18:07:30 -070055 public static final int MAIN_COLOR_DARK = 0xff212121;
Lucas Dupinc77b71d2017-07-05 17:34:41 -070056
Lucas Dupin6e69c852017-07-06 16:09:24 -070057 private final TonalPalette mGreyPalette;
58 private final ArrayList<TonalPalette> mTonalPalettes;
59 private final ArrayList<ColorRange> mBlacklistedColors;
60
Lucas Dupin7aaa3532017-05-28 08:51:07 -070061 // Temporary variable to avoid allocations
62 private float[] mTmpHSL = new float[3];
Lucas Dupinccec5b62017-05-08 18:35:34 -070063
Lucas Dupin6e69c852017-07-06 16:09:24 -070064 public Tonal(Context context) {
65
66 ConfigParser parser = new ConfigParser(context);
67 mTonalPalettes = parser.getTonalPalettes();
68 mBlacklistedColors = parser.getBlacklistedColors();
69
70 mGreyPalette = mTonalPalettes.get(0);
71 mTonalPalettes.remove(0);
72 }
73
Lucas Dupinccec5b62017-05-08 18:35:34 -070074 /**
Lucas Dupinc77b71d2017-07-05 17:34:41 -070075 * Grab colors from WallpaperColors and set them into GradientColors.
76 * Also applies the default gradient in case extraction fails.
Lucas Dupinccec5b62017-05-08 18:35:34 -070077 *
Lucas Dupinc77b71d2017-07-05 17:34:41 -070078 * @param inWallpaperColors Input.
79 * @param outColorsNormal Colors for normal theme.
80 * @param outColorsDark Colors for dar theme.
81 * @param outColorsExtraDark Colors for extra dark theme.
Lucas Dupinccec5b62017-05-08 18:35:34 -070082 */
Lucas Dupinc77b71d2017-07-05 17:34:41 -070083 public void extractInto(@Nullable WallpaperColors inWallpaperColors,
84 @NonNull GradientColors outColorsNormal, @NonNull GradientColors outColorsDark,
85 @NonNull GradientColors outColorsExtraDark) {
86 boolean success = runTonalExtraction(inWallpaperColors, outColorsNormal, outColorsDark,
87 outColorsExtraDark);
88 if (!success) {
89 applyFallback(inWallpaperColors, outColorsNormal, outColorsDark, outColorsExtraDark);
90 }
91 }
92
93 /**
94 * Grab colors from WallpaperColors and set them into GradientColors.
95 *
96 * @param inWallpaperColors Input.
97 * @param outColorsNormal Colors for normal theme.
98 * @param outColorsDark Colors for dar theme.
99 * @param outColorsExtraDark Colors for extra dark theme.
100 * @return True if succeeded or false if failed.
101 */
102 private boolean runTonalExtraction(@Nullable WallpaperColors inWallpaperColors,
Lucas Dupin7aaa3532017-05-28 08:51:07 -0700103 @NonNull GradientColors outColorsNormal, @NonNull GradientColors outColorsDark,
104 @NonNull GradientColors outColorsExtraDark) {
Lucas Dupin65f56582017-04-27 10:21:41 -0700105
Lucas Dupinc77b71d2017-07-05 17:34:41 -0700106 if (inWallpaperColors == null) {
107 return false;
108 }
109
Lucas Dupin84b89d92017-05-09 12:16:19 -0700110 final List<Color> mainColors = inWallpaperColors.getMainColors();
111 final int mainColorsSize = mainColors.size();
Lucas Dupine2efebc2017-08-11 10:30:58 -0700112 final int hints = inWallpaperColors.getColorHints();
113 final boolean supportsDarkText = (hints & WallpaperColors.HINT_SUPPORTS_DARK_TEXT) != 0;
114 final boolean generatedFromBitmap = (hints & WallpaperColors.HINT_FROM_BITMAP) != 0;
Lucas Dupin84b89d92017-05-09 12:16:19 -0700115
116 if (mainColorsSize == 0) {
Lucas Dupinccec5b62017-05-08 18:35:34 -0700117 return false;
Lucas Dupin65f56582017-04-27 10:21:41 -0700118 }
Lucas Dupin65f56582017-04-27 10:21:41 -0700119
Lucas Dupine2efebc2017-08-11 10:30:58 -0700120 // Decide what's the best color to use.
121 // We have 2 options:
122 // • Just pick the primary color
123 // • Filter out blacklisted colors. This is useful when palette is generated
124 // automatically from a bitmap.
Lucas Dupin84b89d92017-05-09 12:16:19 -0700125 Color bestColor = null;
126 final float[] hsl = new float[3];
127 for (int i = 0; i < mainColorsSize; i++) {
128 final Color color = mainColors.get(i);
129 final int colorValue = color.toArgb();
Lucas Dupin65f56582017-04-27 10:21:41 -0700130 ColorUtils.RGBToHSL(Color.red(colorValue), Color.green(colorValue),
131 Color.blue(colorValue), hsl);
Lucas Dupin98ce4582017-05-22 19:43:45 -0700132
133 // Stop when we find a color that meets our criteria
Lucas Dupine2efebc2017-08-11 10:30:58 -0700134 if (!generatedFromBitmap || !isBlacklisted(hsl)) {
Lucas Dupin84b89d92017-05-09 12:16:19 -0700135 bestColor = color;
Lucas Dupinccec5b62017-05-08 18:35:34 -0700136 break;
Lucas Dupin65f56582017-04-27 10:21:41 -0700137 }
138 }
139
Lucas Dupin7aaa3532017-05-28 08:51:07 -0700140 // Fail if not found
Lucas Dupin65f56582017-04-27 10:21:41 -0700141 if (bestColor == null) {
Lucas Dupin98ce4582017-05-22 19:43:45 -0700142 return false;
Lucas Dupin65f56582017-04-27 10:21:41 -0700143 }
144
Lucas Dupine2efebc2017-08-11 10:30:58 -0700145 // Tonal is not really a sort, it takes a color from the extracted
146 // palette and finds a best fit amongst a collection of pre-defined
147 // palettes. The best fit is tweaked to be closer to the source color
148 // and replaces the original palette.
Lucas Dupin84b89d92017-05-09 12:16:19 -0700149 int colorValue = bestColor.toArgb();
Lucas Dupin65f56582017-04-27 10:21:41 -0700150 ColorUtils.RGBToHSL(Color.red(colorValue), Color.green(colorValue), Color.blue(colorValue),
151 hsl);
Lucas Dupin98ce4582017-05-22 19:43:45 -0700152
153 // The Android HSL definition requires the hue to go from 0 to 360 but
Lucas Dupin7aaa3532017-05-28 08:51:07 -0700154 // the Material Tonal Palette defines hues from 0 to 1.
155 hsl[0] /= 360f;
Lucas Dupin65f56582017-04-27 10:21:41 -0700156
Lucas Dupinccec5b62017-05-08 18:35:34 -0700157 // Find the palette that contains the closest color
Lucas Dupin5266ad12017-06-17 20:57:28 -0700158 TonalPalette palette = findTonalPalette(hsl[0], hsl[1]);
Lucas Dupin65f56582017-04-27 10:21:41 -0700159 if (palette == null) {
160 Log.w(TAG, "Could not find a tonal palette!");
Lucas Dupinccec5b62017-05-08 18:35:34 -0700161 return false;
Lucas Dupin65f56582017-04-27 10:21:41 -0700162 }
163
Lucas Dupin7aaa3532017-05-28 08:51:07 -0700164 // Figure out what's the main color index in the optimal palette
Lucas Dupin65f56582017-04-27 10:21:41 -0700165 int fitIndex = bestFit(palette, hsl[0], hsl[1], hsl[2]);
166 if (fitIndex == -1) {
167 Log.w(TAG, "Could not find best fit!");
Lucas Dupinccec5b62017-05-08 18:35:34 -0700168 return false;
Lucas Dupin65f56582017-04-27 10:21:41 -0700169 }
Lucas Dupin7aaa3532017-05-28 08:51:07 -0700170
171 // Generate the 10 colors palette by offsetting each one of them
Lucas Dupin65f56582017-04-27 10:21:41 -0700172 float[] h = fit(palette.h, hsl[0], fitIndex,
173 Float.NEGATIVE_INFINITY, Float.POSITIVE_INFINITY);
174 float[] s = fit(palette.s, hsl[1], fitIndex, 0.0f, 1.0f);
175 float[] l = fit(palette.l, hsl[2], fitIndex, 0.0f, 1.0f);
Robert Snoeberger0397c842019-02-07 14:25:46 -0500176 int[] colorPalette = getColorPalette(h, s, l);
Lucas Dupin65f56582017-04-27 10:21:41 -0700177
Lucas Dupin7aaa3532017-05-28 08:51:07 -0700178 if (DEBUG) {
179 StringBuilder builder = new StringBuilder("Tonal Palette - index: " + fitIndex +
180 ". Main color: " + Integer.toHexString(getColorInt(fitIndex, h, s, l)) +
181 "\nColors: ");
Lucas Dupin65f56582017-04-27 10:21:41 -0700182
Lucas Dupin7aaa3532017-05-28 08:51:07 -0700183 for (int i=0; i < h.length; i++) {
184 builder.append(Integer.toHexString(getColorInt(i, h, s, l)));
185 if (i < h.length - 1) {
186 builder.append(", ");
Lucas Dupinccec5b62017-05-08 18:35:34 -0700187 }
Lucas Dupinccec5b62017-05-08 18:35:34 -0700188 }
Lucas Dupin7aaa3532017-05-28 08:51:07 -0700189 Log.d(TAG, builder.toString());
Lucas Dupinccec5b62017-05-08 18:35:34 -0700190 }
191
Lucas Dupinc77b71d2017-07-05 17:34:41 -0700192 int primaryIndex = fitIndex;
193 int mainColor = getColorInt(primaryIndex, h, s, l);
194
195 // We might want use the fallback in case the extracted color is brighter than our
196 // light fallback or darker than our dark fallback.
197 ColorUtils.colorToHSL(mainColor, mTmpHSL);
198 final float mainLuminosity = mTmpHSL[2];
Lucas Dupind26facc2018-09-10 18:07:30 -0700199 ColorUtils.colorToHSL(MAIN_COLOR_LIGHT, mTmpHSL);
Lucas Dupinc77b71d2017-07-05 17:34:41 -0700200 final float lightLuminosity = mTmpHSL[2];
201 if (mainLuminosity > lightLuminosity) {
202 return false;
203 }
Lucas Dupind26facc2018-09-10 18:07:30 -0700204 ColorUtils.colorToHSL(MAIN_COLOR_DARK, mTmpHSL);
Lucas Dupinc77b71d2017-07-05 17:34:41 -0700205 final float darkLuminosity = mTmpHSL[2];
206 if (mainLuminosity < darkLuminosity) {
207 return false;
208 }
209
Lucas Dupin7aaa3532017-05-28 08:51:07 -0700210 // Normal colors:
Lucas Dupinc77b71d2017-07-05 17:34:41 -0700211 outColorsNormal.setMainColor(mainColor);
Lucas Dupin4dcacd32018-01-05 11:36:51 -0800212 outColorsNormal.setSecondaryColor(mainColor);
Robert Snoeberger0397c842019-02-07 14:25:46 -0500213 outColorsNormal.setColorPalette(colorPalette);
Lucas Dupin7aaa3532017-05-28 08:51:07 -0700214
215 // Dark colors:
216 // Stops at 4th color, only lighter if dark text is supported
Lucas Dupinc77b71d2017-07-05 17:34:41 -0700217 if (supportsDarkText) {
Lucas Dupin7aaa3532017-05-28 08:51:07 -0700218 primaryIndex = h.length - 1;
Lucas Dupinc77b71d2017-07-05 17:34:41 -0700219 } else if (fitIndex < 2) {
220 primaryIndex = 0;
221 } else {
222 primaryIndex = Math.min(fitIndex, 3);
Lucas Dupin7aaa3532017-05-28 08:51:07 -0700223 }
Lucas Dupin4dcacd32018-01-05 11:36:51 -0800224 mainColor = getColorInt(primaryIndex, h, s, l);
225 outColorsDark.setMainColor(mainColor);
226 outColorsDark.setSecondaryColor(mainColor);
Robert Snoeberger0397c842019-02-07 14:25:46 -0500227 outColorsDark.setColorPalette(colorPalette);
Lucas Dupin7aaa3532017-05-28 08:51:07 -0700228
229 // Extra Dark:
230 // Stay close to dark colors until dark text is supported
Lucas Dupinc77b71d2017-07-05 17:34:41 -0700231 if (supportsDarkText) {
Lucas Dupin7aaa3532017-05-28 08:51:07 -0700232 primaryIndex = h.length - 1;
Lucas Dupinc77b71d2017-07-05 17:34:41 -0700233 } else if (fitIndex < 2) {
234 primaryIndex = 0;
235 } else {
236 primaryIndex = 2;
Lucas Dupin7aaa3532017-05-28 08:51:07 -0700237 }
Lucas Dupin4dcacd32018-01-05 11:36:51 -0800238 mainColor = getColorInt(primaryIndex, h, s, l);
239 outColorsExtraDark.setMainColor(mainColor);
240 outColorsExtraDark.setSecondaryColor(mainColor);
Robert Snoeberger0397c842019-02-07 14:25:46 -0500241 outColorsExtraDark.setColorPalette(colorPalette);
Lucas Dupin7aaa3532017-05-28 08:51:07 -0700242
Lucas Dupin7aaa3532017-05-28 08:51:07 -0700243 outColorsNormal.setSupportsDarkText(supportsDarkText);
244 outColorsDark.setSupportsDarkText(supportsDarkText);
245 outColorsExtraDark.setSupportsDarkText(supportsDarkText);
246
247 if (DEBUG) {
248 Log.d(TAG, "Gradients: \n\tNormal " + outColorsNormal + "\n\tDark " + outColorsDark
Lucas Dupin6e69c852017-07-06 16:09:24 -0700249 + "\n\tExtra dark: " + outColorsExtraDark);
Lucas Dupin7aaa3532017-05-28 08:51:07 -0700250 }
Lucas Dupinccec5b62017-05-08 18:35:34 -0700251
252 return true;
Lucas Dupin65f56582017-04-27 10:21:41 -0700253 }
254
Lucas Dupinc77b71d2017-07-05 17:34:41 -0700255 private void applyFallback(@Nullable WallpaperColors inWallpaperColors,
256 GradientColors outColorsNormal, GradientColors outColorsDark,
257 GradientColors outColorsExtraDark) {
258 applyFallback(inWallpaperColors, outColorsNormal);
259 applyFallback(inWallpaperColors, outColorsDark);
260 applyFallback(inWallpaperColors, outColorsExtraDark);
261 }
262
263 /**
264 * Sets the gradient to the light or dark fallbacks based on the current wallpaper colors.
265 *
266 * @param inWallpaperColors Colors to read.
267 * @param outGradientColors Destination.
268 */
Robert Snoeberger0397c842019-02-07 14:25:46 -0500269 public void applyFallback(@Nullable WallpaperColors inWallpaperColors,
Lucas Dupinc77b71d2017-07-05 17:34:41 -0700270 @NonNull GradientColors outGradientColors) {
271 boolean light = inWallpaperColors != null
272 && (inWallpaperColors.getColorHints() & WallpaperColors.HINT_SUPPORTS_DARK_TEXT)
273 != 0;
Lucas Dupin4dcacd32018-01-05 11:36:51 -0800274 final int color = light ? MAIN_COLOR_LIGHT : MAIN_COLOR_DARK;
Robert Snoeberger0397c842019-02-07 14:25:46 -0500275 final float[] hsl = new float[3];
276 ColorUtils.colorToHSL(color, hsl);
Lucas Dupinc77b71d2017-07-05 17:34:41 -0700277
Lucas Dupin4dcacd32018-01-05 11:36:51 -0800278 outGradientColors.setMainColor(color);
279 outGradientColors.setSecondaryColor(color);
Lucas Dupinc77b71d2017-07-05 17:34:41 -0700280 outGradientColors.setSupportsDarkText(light);
Robert Snoeberger0397c842019-02-07 14:25:46 -0500281 outGradientColors.setColorPalette(getColorPalette(findTonalPalette(hsl[0], hsl[1])));
Lucas Dupinc77b71d2017-07-05 17:34:41 -0700282 }
283
Lucas Dupin7aaa3532017-05-28 08:51:07 -0700284 private int getColorInt(int fitIndex, float[] h, float[] s, float[] l) {
285 mTmpHSL[0] = fract(h[fitIndex]) * 360.0f;
286 mTmpHSL[1] = s[fitIndex];
287 mTmpHSL[2] = l[fitIndex];
288 return ColorUtils.HSLToColor(mTmpHSL);
289 }
290
Robert Snoeberger0397c842019-02-07 14:25:46 -0500291 private int[] getColorPalette(float[] h, float[] s, float[] l) {
292 int[] colorPalette = new int[h.length];
293 for (int i = 0; i < colorPalette.length; i++) {
294 colorPalette[i] = getColorInt(i, h, s, l);
295 }
296 return colorPalette;
297 }
298
299 private int[] getColorPalette(TonalPalette palette) {
300 return getColorPalette(palette.h, palette.s, palette.l);
301 }
302
303
Lucas Dupin98ce4582017-05-22 19:43:45 -0700304 /**
305 * Checks if a given color exists in the blacklist
306 * @param hsl float array with 3 components (H 0..360, S 0..1 and L 0..1)
307 * @return true if color should be avoided
308 */
309 private boolean isBlacklisted(float[] hsl) {
Lucas Dupin6e69c852017-07-06 16:09:24 -0700310 for (int i = mBlacklistedColors.size() - 1; i >= 0; i--) {
311 ColorRange badRange = mBlacklistedColors.get(i);
Lucas Dupin98ce4582017-05-22 19:43:45 -0700312 if (badRange.containsColor(hsl[0], hsl[1], hsl[2])) {
313 return true;
314 }
315 }
316 return false;
317 }
318
Lucas Dupin65f56582017-04-27 10:21:41 -0700319 /**
320 * Offsets all colors by a delta, clamping values that go beyond what's
321 * supported on the color space.
322 * @param data what you want to fit
323 * @param v how big should be the offset
324 * @param index which index to calculate the delta against
325 * @param min minimum accepted value (clamp)
326 * @param max maximum accepted value (clamp)
Lucas Dupinccec5b62017-05-08 18:35:34 -0700327 * @return new shifted palette
Lucas Dupin65f56582017-04-27 10:21:41 -0700328 */
329 private static float[] fit(float[] data, float v, int index, float min, float max) {
330 float[] fitData = new float[data.length];
331 float delta = v - data[index];
332
333 for (int i = 0; i < data.length; i++) {
334 fitData[i] = MathUtils.constrain(data[i] + delta, min, max);
335 }
336
337 return fitData;
338 }
339
Lucas Dupin65f56582017-04-27 10:21:41 -0700340 /**
341 * Finds the closest color in a palette, given another HSL color
342 *
343 * @param palette where to search
344 * @param h hue
345 * @param s saturation
346 * @param l lightness
347 * @return closest index or -1 if palette is empty.
348 */
349 private static int bestFit(@NonNull TonalPalette palette, float h, float s, float l) {
350 int minErrorIndex = -1;
351 float minError = Float.POSITIVE_INFINITY;
352
353 for (int i = 0; i < palette.h.length; i++) {
354 float error =
355 FIT_WEIGHT_H * Math.abs(h - palette.h[i])
356 + FIT_WEIGHT_S * Math.abs(s - palette.s[i])
357 + FIT_WEIGHT_L * Math.abs(l - palette.l[i]);
358 if (error < minError) {
359 minError = error;
360 minErrorIndex = i;
361 }
362 }
363
364 return minErrorIndex;
365 }
366
Lucas Dupin6e69c852017-07-06 16:09:24 -0700367 @VisibleForTesting
368 public List<ColorRange> getBlacklistedColors() {
369 return mBlacklistedColors;
370 }
371
Lucas Dupin65f56582017-04-27 10:21:41 -0700372 @Nullable
Lucas Dupin6e69c852017-07-06 16:09:24 -0700373 private TonalPalette findTonalPalette(float h, float s) {
Lucas Dupin5266ad12017-06-17 20:57:28 -0700374 // Fallback to a grey palette if the color is too desaturated.
375 // This avoids hue shifts.
376 if (s < 0.05f) {
Lucas Dupin6e69c852017-07-06 16:09:24 -0700377 return mGreyPalette;
Lucas Dupin5266ad12017-06-17 20:57:28 -0700378 }
379
Lucas Dupin65f56582017-04-27 10:21:41 -0700380 TonalPalette best = null;
381 float error = Float.POSITIVE_INFINITY;
382
Lucas Dupin6e69c852017-07-06 16:09:24 -0700383 final int tonalPalettesCount = mTonalPalettes.size();
384 for (int i = 0; i < tonalPalettesCount; i++) {
385 final TonalPalette candidate = mTonalPalettes.get(i);
Lucas Dupin84b89d92017-05-09 12:16:19 -0700386
Lucas Dupin65f56582017-04-27 10:21:41 -0700387 if (h >= candidate.minHue && h <= candidate.maxHue) {
388 best = candidate;
389 break;
390 }
391
392 if (candidate.maxHue > 1.0f && h >= 0.0f && h <= fract(candidate.maxHue)) {
393 best = candidate;
394 break;
395 }
396
397 if (candidate.minHue < 0.0f && h >= fract(candidate.minHue) && h <= 1.0f) {
398 best = candidate;
399 break;
400 }
401
402 if (h <= candidate.minHue && candidate.minHue - h < error) {
403 best = candidate;
404 error = candidate.minHue - h;
405 } else if (h >= candidate.maxHue && h - candidate.maxHue < error) {
406 best = candidate;
407 error = h - candidate.maxHue;
408 } else if (candidate.maxHue > 1.0f && h >= fract(candidate.maxHue)
409 && h - fract(candidate.maxHue) < error) {
410 best = candidate;
411 error = h - fract(candidate.maxHue);
412 } else if (candidate.minHue < 0.0f && h <= fract(candidate.minHue)
413 && fract(candidate.minHue) - h < error) {
414 best = candidate;
415 error = fract(candidate.minHue) - h;
416 }
417 }
418
419 return best;
420 }
421
422 private static float fract(float v) {
423 return v - (float) Math.floor(v);
424 }
425
Lucas Dupin47e7cfd2018-02-05 16:53:28 -0800426 @VisibleForTesting
427 public static class TonalPalette {
428 public final float[] h;
429 public final float[] s;
430 public final float[] l;
431 public final float minHue;
432 public final float maxHue;
Lucas Dupin65f56582017-04-27 10:21:41 -0700433
434 TonalPalette(float[] h, float[] s, float[] l) {
Lucas Dupin96a14c32017-06-22 15:35:15 -0700435 if (h.length != s.length || s.length != l.length) {
436 throw new IllegalArgumentException("All arrays should have the same size. h: "
437 + Arrays.toString(h) + " s: " + Arrays.toString(s) + " l: "
438 + Arrays.toString(l));
439 }
Lucas Dupin65f56582017-04-27 10:21:41 -0700440 this.h = h;
441 this.s = s;
442 this.l = l;
443
444 float minHue = Float.POSITIVE_INFINITY;
445 float maxHue = Float.NEGATIVE_INFINITY;
446
447 for (float v : h) {
448 minHue = Math.min(v, minHue);
449 maxHue = Math.max(v, maxHue);
450 }
451
452 this.minHue = minHue;
453 this.maxHue = maxHue;
454 }
455 }
456
Lucas Dupin98ce4582017-05-22 19:43:45 -0700457 /**
458 * Representation of an HSL color range.
459 * <ul>
460 * <li>hsl[0] is Hue [0 .. 360)</li>
461 * <li>hsl[1] is Saturation [0...1]</li>
462 * <li>hsl[2] is Lightness [0...1]</li>
463 * </ul>
464 */
465 @VisibleForTesting
Lucas Dupine2292a92017-07-06 14:35:30 -0700466 public static class ColorRange {
Lucas Dupin98ce4582017-05-22 19:43:45 -0700467 private Range<Float> mHue;
468 private Range<Float> mSaturation;
469 private Range<Float> mLightness;
470
Lucas Dupine2292a92017-07-06 14:35:30 -0700471 public ColorRange(Range<Float> hue, Range<Float> saturation, Range<Float> lightness) {
Lucas Dupin98ce4582017-05-22 19:43:45 -0700472 mHue = hue;
473 mSaturation = saturation;
474 mLightness = lightness;
475 }
476
Lucas Dupine2292a92017-07-06 14:35:30 -0700477 public boolean containsColor(float h, float s, float l) {
Lucas Dupin98ce4582017-05-22 19:43:45 -0700478 if (!mHue.contains(h)) {
479 return false;
480 } else if (!mSaturation.contains(s)) {
481 return false;
482 } else if (!mLightness.contains(l)) {
483 return false;
484 }
485 return true;
486 }
487
Lucas Dupine2292a92017-07-06 14:35:30 -0700488 public float[] getCenter() {
Lucas Dupin98ce4582017-05-22 19:43:45 -0700489 return new float[] {
490 mHue.getLower() + (mHue.getUpper() - mHue.getLower()) / 2f,
491 mSaturation.getLower() + (mSaturation.getUpper() - mSaturation.getLower()) / 2f,
492 mLightness.getLower() + (mLightness.getUpper() - mLightness.getLower()) / 2f
493 };
494 }
495
496 @Override
497 public String toString() {
498 return String.format("H: %s, S: %s, L %s", mHue, mSaturation, mLightness);
499 }
500 }
Lucas Dupin6e69c852017-07-06 16:09:24 -0700501
502 @VisibleForTesting
503 public static class ConfigParser {
504 private final ArrayList<TonalPalette> mTonalPalettes;
505 private final ArrayList<ColorRange> mBlacklistedColors;
506
507 public ConfigParser(Context context) {
508 mTonalPalettes = new ArrayList<>();
509 mBlacklistedColors = new ArrayList<>();
510
511 // Load all palettes and the blacklist from an XML.
512 try {
513 XmlPullParser parser = context.getResources().getXml(R.xml.color_extraction);
514 int eventType = parser.getEventType();
515 while (eventType != XmlPullParser.END_DOCUMENT) {
516 if (eventType == XmlPullParser.START_DOCUMENT ||
517 eventType == XmlPullParser.END_TAG) {
518 // just skip
519 } else if (eventType == XmlPullParser.START_TAG) {
520 String tagName = parser.getName();
521 if (tagName.equals("palettes")) {
522 parsePalettes(parser);
523 } else if (tagName.equals("blacklist")) {
524 parseBlacklist(parser);
525 }
526 } else {
527 throw new XmlPullParserException("Invalid XML event " + eventType + " - "
528 + parser.getName(), parser, null);
529 }
530 eventType = parser.next();
531 }
532 } catch (XmlPullParserException | IOException e) {
533 throw new RuntimeException(e);
534 }
535 }
536
537 public ArrayList<TonalPalette> getTonalPalettes() {
538 return mTonalPalettes;
539 }
540
541 public ArrayList<ColorRange> getBlacklistedColors() {
542 return mBlacklistedColors;
543 }
544
545 private void parseBlacklist(XmlPullParser parser)
546 throws XmlPullParserException, IOException {
547 parser.require(XmlPullParser.START_TAG, null, "blacklist");
548 while (parser.next() != XmlPullParser.END_TAG) {
549 if (parser.getEventType() != XmlPullParser.START_TAG) {
550 continue;
551 }
552 String name = parser.getName();
553 // Starts by looking for the entry tag
554 if (name.equals("range")) {
555 mBlacklistedColors.add(readRange(parser));
556 parser.next();
557 } else {
558 throw new XmlPullParserException("Invalid tag: " + name, parser, null);
559 }
560 }
561 }
562
563 private ColorRange readRange(XmlPullParser parser)
564 throws XmlPullParserException, IOException {
565 parser.require(XmlPullParser.START_TAG, null, "range");
566 float[] h = readFloatArray(parser.getAttributeValue(null, "h"));
567 float[] s = readFloatArray(parser.getAttributeValue(null, "s"));
568 float[] l = readFloatArray(parser.getAttributeValue(null, "l"));
569
570 if (h == null || s == null || l == null) {
571 throw new XmlPullParserException("Incomplete range tag.", parser, null);
572 }
573
574 return new ColorRange(new Range<>(h[0], h[1]), new Range<>(s[0], s[1]),
575 new Range<>(l[0], l[1]));
576 }
577
578 private void parsePalettes(XmlPullParser parser)
579 throws XmlPullParserException, IOException {
580 parser.require(XmlPullParser.START_TAG, null, "palettes");
581 while (parser.next() != XmlPullParser.END_TAG) {
582 if (parser.getEventType() != XmlPullParser.START_TAG) {
583 continue;
584 }
585 String name = parser.getName();
586 // Starts by looking for the entry tag
587 if (name.equals("palette")) {
588 mTonalPalettes.add(readPalette(parser));
589 parser.next();
590 } else {
591 throw new XmlPullParserException("Invalid tag: " + name);
592 }
593 }
594 }
595
596 private TonalPalette readPalette(XmlPullParser parser)
597 throws XmlPullParserException, IOException {
598 parser.require(XmlPullParser.START_TAG, null, "palette");
599
600 float[] h = readFloatArray(parser.getAttributeValue(null, "h"));
601 float[] s = readFloatArray(parser.getAttributeValue(null, "s"));
602 float[] l = readFloatArray(parser.getAttributeValue(null, "l"));
603
604 if (h == null || s == null || l == null) {
605 throw new XmlPullParserException("Incomplete range tag.", parser, null);
606 }
607
608 return new TonalPalette(h, s, l);
609 }
610
611 private float[] readFloatArray(String attributeValue)
612 throws IOException, XmlPullParserException {
613 String[] tokens = attributeValue.replaceAll(" ", "").replaceAll("\n", "").split(",");
614 float[] numbers = new float[tokens.length];
615 for (int i = 0; i < tokens.length; i++) {
616 numbers[i] = Float.parseFloat(tokens[i]);
617 }
618 return numbers;
619 }
620 }
Robert Snoeberger0397c842019-02-07 14:25:46 -0500621}