OSDN Git Service

Verify all possible hosts that match web nav
[android-x86/frameworks-base.git] / core / java / com / android / internal / colorextraction / types / Tonal.java
1 /*
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
17 package com.android.internal.colorextraction.types;
18
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.app.WallpaperColors;
22 import android.content.Context;
23 import android.graphics.Color;
24 import android.util.Log;
25 import android.util.MathUtils;
26 import android.util.Range;
27
28 import com.android.internal.R;
29 import com.android.internal.annotations.VisibleForTesting;
30 import com.android.internal.colorextraction.ColorExtractor.GradientColors;
31 import com.android.internal.graphics.ColorUtils;
32
33 import org.xmlpull.v1.XmlPullParser;
34 import org.xmlpull.v1.XmlPullParserException;
35
36 import java.io.IOException;
37 import java.util.ArrayList;
38 import java.util.Arrays;
39 import java.util.List;
40
41 /**
42  * Implementation of tonal color extraction
43  */
44 public 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
52     private static final boolean DEBUG = true;
53
54     public static final int MAIN_COLOR_LIGHT = 0xffe0e0e0;
55     public static final int SECONDARY_COLOR_LIGHT = 0xff9e9e9e;
56     public static final int MAIN_COLOR_DARK = 0xff212121;
57     public static final int SECONDARY_COLOR_DARK = 0xff000000;
58
59     private final TonalPalette mGreyPalette;
60     private final ArrayList<TonalPalette> mTonalPalettes;
61     private final ArrayList<ColorRange> mBlacklistedColors;
62
63     // Temporary variable to avoid allocations
64     private float[] mTmpHSL = new float[3];
65
66     public Tonal(Context context) {
67
68         ConfigParser parser = new ConfigParser(context);
69         mTonalPalettes = parser.getTonalPalettes();
70         mBlacklistedColors = parser.getBlacklistedColors();
71
72         mGreyPalette = mTonalPalettes.get(0);
73         mTonalPalettes.remove(0);
74     }
75
76     /**
77      * Grab colors from WallpaperColors and set them into GradientColors.
78      * Also applies the default gradient in case extraction fails.
79      *
80      * @param inWallpaperColors Input.
81      * @param outColorsNormal Colors for normal theme.
82      * @param outColorsDark Colors for dar theme.
83      * @param outColorsExtraDark Colors for extra dark theme.
84      */
85     public void extractInto(@Nullable WallpaperColors inWallpaperColors,
86             @NonNull GradientColors outColorsNormal, @NonNull GradientColors outColorsDark,
87             @NonNull GradientColors outColorsExtraDark) {
88         boolean success = runTonalExtraction(inWallpaperColors, outColorsNormal, outColorsDark,
89                 outColorsExtraDark);
90         if (!success) {
91             applyFallback(inWallpaperColors, outColorsNormal, outColorsDark, outColorsExtraDark);
92         }
93     }
94
95     /**
96      * Grab colors from WallpaperColors and set them into GradientColors.
97      *
98      * @param inWallpaperColors Input.
99      * @param outColorsNormal Colors for normal theme.
100      * @param outColorsDark Colors for dar theme.
101      * @param outColorsExtraDark Colors for extra dark theme.
102      * @return True if succeeded or false if failed.
103      */
104     private boolean runTonalExtraction(@Nullable WallpaperColors inWallpaperColors,
105             @NonNull GradientColors outColorsNormal, @NonNull GradientColors outColorsDark,
106             @NonNull GradientColors outColorsExtraDark) {
107
108         if (inWallpaperColors == null) {
109             return false;
110         }
111
112         final List<Color> mainColors = inWallpaperColors.getMainColors();
113         final int mainColorsSize = mainColors.size();
114         final int hints = inWallpaperColors.getColorHints();
115         final boolean supportsDarkText = (hints & WallpaperColors.HINT_SUPPORTS_DARK_TEXT) != 0;
116         final boolean generatedFromBitmap = (hints & WallpaperColors.HINT_FROM_BITMAP) != 0;
117
118         if (mainColorsSize == 0) {
119             return false;
120         }
121
122         // Decide what's the best color to use.
123         // We have 2 options:
124         // • Just pick the primary color
125         // • Filter out blacklisted colors. This is useful when palette is generated
126         //   automatically from a bitmap.
127         Color bestColor = null;
128         final float[] hsl = new float[3];
129         for (int i = 0; i < mainColorsSize; i++) {
130             final Color color = mainColors.get(i);
131             final int colorValue = color.toArgb();
132             ColorUtils.RGBToHSL(Color.red(colorValue), Color.green(colorValue),
133                     Color.blue(colorValue), hsl);
134
135             // Stop when we find a color that meets our criteria
136             if (!generatedFromBitmap || !isBlacklisted(hsl)) {
137                 bestColor = color;
138                 break;
139             }
140         }
141
142         // Fail if not found
143         if (bestColor == null) {
144             return false;
145         }
146
147         // Tonal is not really a sort, it takes a color from the extracted
148         // palette and finds a best fit amongst a collection of pre-defined
149         // palettes. The best fit is tweaked to be closer to the source color
150         // and replaces the original palette.
151         int colorValue = bestColor.toArgb();
152         ColorUtils.RGBToHSL(Color.red(colorValue), Color.green(colorValue), Color.blue(colorValue),
153                 hsl);
154
155         // The Android HSL definition requires the hue to go from 0 to 360 but
156         // the Material Tonal Palette defines hues from 0 to 1.
157         hsl[0] /= 360f;
158
159         // Find the palette that contains the closest color
160         TonalPalette palette = findTonalPalette(hsl[0], hsl[1]);
161         if (palette == null) {
162             Log.w(TAG, "Could not find a tonal palette!");
163             return false;
164         }
165
166         // Figure out what's the main color index in the optimal palette
167         int fitIndex = bestFit(palette, hsl[0], hsl[1], hsl[2]);
168         if (fitIndex == -1) {
169             Log.w(TAG, "Could not find best fit!");
170             return false;
171         }
172
173         // Generate the 10 colors palette by offsetting each one of them
174         float[] h = fit(palette.h, hsl[0], fitIndex,
175                 Float.NEGATIVE_INFINITY, Float.POSITIVE_INFINITY);
176         float[] s = fit(palette.s, hsl[1], fitIndex, 0.0f, 1.0f);
177         float[] l = fit(palette.l, hsl[2], fitIndex, 0.0f, 1.0f);
178
179         if (DEBUG) {
180             StringBuilder builder = new StringBuilder("Tonal Palette - index: " + fitIndex +
181                     ". Main color: " + Integer.toHexString(getColorInt(fitIndex, h, s, l)) +
182                     "\nColors: ");
183
184             for (int i=0; i < h.length; i++) {
185                 builder.append(Integer.toHexString(getColorInt(i, h, s, l)));
186                 if (i < h.length - 1) {
187                     builder.append(", ");
188                 }
189             }
190             Log.d(TAG, builder.toString());
191         }
192
193         int primaryIndex = fitIndex;
194         int mainColor = getColorInt(primaryIndex, h, s, l);
195
196         // We might want use the fallback in case the extracted color is brighter than our
197         // light fallback or darker than our dark fallback.
198         ColorUtils.colorToHSL(mainColor, mTmpHSL);
199         final float mainLuminosity = mTmpHSL[2];
200         ColorUtils.colorToHSL(MAIN_COLOR_LIGHT, mTmpHSL);
201         final float lightLuminosity = mTmpHSL[2];
202         if (mainLuminosity > lightLuminosity) {
203             return false;
204         }
205         ColorUtils.colorToHSL(MAIN_COLOR_DARK, mTmpHSL);
206         final float darkLuminosity = mTmpHSL[2];
207         if (mainLuminosity < darkLuminosity) {
208             return false;
209         }
210
211         // Normal colors:
212         // best fit + a 2 colors offset
213         outColorsNormal.setMainColor(mainColor);
214         int secondaryIndex = primaryIndex + (primaryIndex >= 2 ? -2 : 2);
215         outColorsNormal.setSecondaryColor(getColorInt(secondaryIndex, h, s, l));
216
217         // Dark colors:
218         // Stops at 4th color, only lighter if dark text is supported
219         if (supportsDarkText) {
220             primaryIndex = h.length - 1;
221         } else if (fitIndex < 2) {
222             primaryIndex = 0;
223         } else {
224             primaryIndex = Math.min(fitIndex, 3);
225         }
226         secondaryIndex = primaryIndex + (primaryIndex >= 2 ? -2 : 2);
227         outColorsDark.setMainColor(getColorInt(primaryIndex, h, s, l));
228         outColorsDark.setSecondaryColor(getColorInt(secondaryIndex, h, s, l));
229
230         // Extra Dark:
231         // Stay close to dark colors until dark text is supported
232         if (supportsDarkText) {
233             primaryIndex = h.length - 1;
234         } else if (fitIndex < 2) {
235             primaryIndex = 0;
236         } else {
237             primaryIndex = 2;
238         }
239         secondaryIndex = primaryIndex + (primaryIndex >= 2 ? -2 : 2);
240         outColorsExtraDark.setMainColor(getColorInt(primaryIndex, h, s, l));
241         outColorsExtraDark.setSecondaryColor(getColorInt(secondaryIndex, h, s, l));
242
243         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
249                     + "\n\tExtra dark: " + outColorsExtraDark);
250         }
251
252         return true;
253     }
254
255     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      */
269     public static void applyFallback(@Nullable WallpaperColors inWallpaperColors,
270             @NonNull GradientColors outGradientColors) {
271         boolean light = inWallpaperColors != null
272                 && (inWallpaperColors.getColorHints() & WallpaperColors.HINT_SUPPORTS_DARK_TEXT)
273                 != 0;
274         int innerColor = light ? MAIN_COLOR_LIGHT : MAIN_COLOR_DARK;
275         int outerColor = light ? SECONDARY_COLOR_LIGHT : SECONDARY_COLOR_DARK;
276
277         outGradientColors.setMainColor(innerColor);
278         outGradientColors.setSecondaryColor(outerColor);
279         outGradientColors.setSupportsDarkText(light);
280     }
281
282     private int getColorInt(int fitIndex, float[] h, float[] s, float[] l) {
283         mTmpHSL[0] = fract(h[fitIndex]) * 360.0f;
284         mTmpHSL[1] = s[fitIndex];
285         mTmpHSL[2] = l[fitIndex];
286         return ColorUtils.HSLToColor(mTmpHSL);
287     }
288
289     /**
290      * Checks if a given color exists in the blacklist
291      * @param hsl float array with 3 components (H 0..360, S 0..1 and L 0..1)
292      * @return true if color should be avoided
293      */
294     private boolean isBlacklisted(float[] hsl) {
295         for (int i = mBlacklistedColors.size() - 1; i >= 0; i--) {
296             ColorRange badRange = mBlacklistedColors.get(i);
297             if (badRange.containsColor(hsl[0], hsl[1], hsl[2])) {
298                 return true;
299             }
300         }
301         return false;
302     }
303
304     /**
305      * Offsets all colors by a delta, clamping values that go beyond what's
306      * supported on the color space.
307      * @param data what you want to fit
308      * @param v how big should be the offset
309      * @param index which index to calculate the delta against
310      * @param min minimum accepted value (clamp)
311      * @param max maximum accepted value (clamp)
312      * @return new shifted palette
313      */
314     private static float[] fit(float[] data, float v, int index, float min, float max) {
315         float[] fitData = new float[data.length];
316         float delta = v - data[index];
317
318         for (int i = 0; i < data.length; i++) {
319             fitData[i] = MathUtils.constrain(data[i] + delta, min, max);
320         }
321
322         return fitData;
323     }
324
325     /**
326      * Finds the closest color in a palette, given another HSL color
327      *
328      * @param palette where to search
329      * @param h hue
330      * @param s saturation
331      * @param l lightness
332      * @return closest index or -1 if palette is empty.
333      */
334     private static int bestFit(@NonNull TonalPalette palette, float h, float s, float l) {
335         int minErrorIndex = -1;
336         float minError = Float.POSITIVE_INFINITY;
337
338         for (int i = 0; i < palette.h.length; i++) {
339             float error =
340                     FIT_WEIGHT_H * Math.abs(h - palette.h[i])
341                             + FIT_WEIGHT_S * Math.abs(s - palette.s[i])
342                             + FIT_WEIGHT_L * Math.abs(l - palette.l[i]);
343             if (error < minError) {
344                 minError = error;
345                 minErrorIndex = i;
346             }
347         }
348
349         return minErrorIndex;
350     }
351
352     @VisibleForTesting
353     public List<ColorRange> getBlacklistedColors() {
354         return mBlacklistedColors;
355     }
356
357     @Nullable
358     private TonalPalette findTonalPalette(float h, float s) {
359         // Fallback to a grey palette if the color is too desaturated.
360         // This avoids hue shifts.
361         if (s < 0.05f) {
362             return mGreyPalette;
363         }
364
365         TonalPalette best = null;
366         float error = Float.POSITIVE_INFINITY;
367
368         final int tonalPalettesCount = mTonalPalettes.size();
369         for (int i = 0; i < tonalPalettesCount; i++) {
370             final TonalPalette candidate = mTonalPalettes.get(i);
371
372             if (h >= candidate.minHue && h <= candidate.maxHue) {
373                 best = candidate;
374                 break;
375             }
376
377             if (candidate.maxHue > 1.0f && h >= 0.0f && h <= fract(candidate.maxHue)) {
378                 best = candidate;
379                 break;
380             }
381
382             if (candidate.minHue < 0.0f && h >= fract(candidate.minHue) && h <= 1.0f) {
383                 best = candidate;
384                 break;
385             }
386
387             if (h <= candidate.minHue && candidate.minHue - h < error) {
388                 best = candidate;
389                 error = candidate.minHue - h;
390             } else if (h >= candidate.maxHue && h - candidate.maxHue < error) {
391                 best = candidate;
392                 error = h - candidate.maxHue;
393             } else if (candidate.maxHue > 1.0f && h >= fract(candidate.maxHue)
394                     && h - fract(candidate.maxHue) < error) {
395                 best = candidate;
396                 error = h - fract(candidate.maxHue);
397             } else if (candidate.minHue < 0.0f && h <= fract(candidate.minHue)
398                     && fract(candidate.minHue) - h < error) {
399                 best = candidate;
400                 error = fract(candidate.minHue) - h;
401             }
402         }
403
404         return best;
405     }
406
407     private static float fract(float v) {
408         return v - (float) Math.floor(v);
409     }
410
411     static class TonalPalette {
412         final float[] h;
413         final float[] s;
414         final float[] l;
415         final float minHue;
416         final float maxHue;
417
418         TonalPalette(float[] h, float[] s, float[] l) {
419             if (h.length != s.length || s.length != l.length) {
420                 throw new IllegalArgumentException("All arrays should have the same size. h: "
421                         + Arrays.toString(h) + " s: " + Arrays.toString(s) + " l: "
422                         + Arrays.toString(l));
423             }
424             this.h = h;
425             this.s = s;
426             this.l = l;
427
428             float minHue = Float.POSITIVE_INFINITY;
429             float maxHue = Float.NEGATIVE_INFINITY;
430
431             for (float v : h) {
432                 minHue = Math.min(v, minHue);
433                 maxHue = Math.max(v, maxHue);
434             }
435
436             this.minHue = minHue;
437             this.maxHue = maxHue;
438         }
439     }
440
441     /**
442      * Representation of an HSL color range.
443      * <ul>
444      * <li>hsl[0] is Hue [0 .. 360)</li>
445      * <li>hsl[1] is Saturation [0...1]</li>
446      * <li>hsl[2] is Lightness [0...1]</li>
447      * </ul>
448      */
449     @VisibleForTesting
450     public static class ColorRange {
451         private Range<Float> mHue;
452         private Range<Float> mSaturation;
453         private Range<Float> mLightness;
454
455         public ColorRange(Range<Float> hue, Range<Float> saturation, Range<Float> lightness) {
456             mHue = hue;
457             mSaturation = saturation;
458             mLightness = lightness;
459         }
460
461         public boolean containsColor(float h, float s, float l) {
462             if (!mHue.contains(h)) {
463                 return false;
464             } else if (!mSaturation.contains(s)) {
465                 return false;
466             } else if (!mLightness.contains(l)) {
467                 return false;
468             }
469             return true;
470         }
471
472         public float[] getCenter() {
473             return new float[] {
474                     mHue.getLower() + (mHue.getUpper() - mHue.getLower()) / 2f,
475                     mSaturation.getLower() + (mSaturation.getUpper() - mSaturation.getLower()) / 2f,
476                     mLightness.getLower() + (mLightness.getUpper() - mLightness.getLower()) / 2f
477             };
478         }
479
480         @Override
481         public String toString() {
482             return String.format("H: %s, S: %s, L %s", mHue, mSaturation, mLightness);
483         }
484     }
485
486     @VisibleForTesting
487     public static class ConfigParser {
488         private final ArrayList<TonalPalette> mTonalPalettes;
489         private final ArrayList<ColorRange> mBlacklistedColors;
490
491         public ConfigParser(Context context) {
492             mTonalPalettes = new ArrayList<>();
493             mBlacklistedColors = new ArrayList<>();
494
495             // Load all palettes and the blacklist from an XML.
496             try {
497                 XmlPullParser parser = context.getResources().getXml(R.xml.color_extraction);
498                 int eventType = parser.getEventType();
499                 while (eventType != XmlPullParser.END_DOCUMENT) {
500                     if (eventType == XmlPullParser.START_DOCUMENT ||
501                             eventType == XmlPullParser.END_TAG) {
502                         // just skip
503                     } else if (eventType == XmlPullParser.START_TAG) {
504                         String tagName = parser.getName();
505                         if (tagName.equals("palettes")) {
506                             parsePalettes(parser);
507                         } else if (tagName.equals("blacklist")) {
508                             parseBlacklist(parser);
509                         }
510                     } else {
511                         throw new XmlPullParserException("Invalid XML event " + eventType + " - "
512                                 + parser.getName(), parser, null);
513                     }
514                     eventType = parser.next();
515                 }
516             } catch (XmlPullParserException | IOException e) {
517                 throw new RuntimeException(e);
518             }
519         }
520
521         public ArrayList<TonalPalette> getTonalPalettes() {
522             return mTonalPalettes;
523         }
524
525         public ArrayList<ColorRange> getBlacklistedColors() {
526             return mBlacklistedColors;
527         }
528
529         private void parseBlacklist(XmlPullParser parser)
530                 throws XmlPullParserException, IOException {
531             parser.require(XmlPullParser.START_TAG, null, "blacklist");
532             while (parser.next() != XmlPullParser.END_TAG) {
533                 if (parser.getEventType() != XmlPullParser.START_TAG) {
534                     continue;
535                 }
536                 String name = parser.getName();
537                 // Starts by looking for the entry tag
538                 if (name.equals("range")) {
539                     mBlacklistedColors.add(readRange(parser));
540                     parser.next();
541                 } else {
542                     throw new XmlPullParserException("Invalid tag: " + name, parser, null);
543                 }
544             }
545         }
546
547         private ColorRange readRange(XmlPullParser parser)
548                 throws XmlPullParserException, IOException {
549             parser.require(XmlPullParser.START_TAG, null, "range");
550             float[] h = readFloatArray(parser.getAttributeValue(null, "h"));
551             float[] s = readFloatArray(parser.getAttributeValue(null, "s"));
552             float[] l = readFloatArray(parser.getAttributeValue(null, "l"));
553
554             if (h == null || s == null || l == null) {
555                 throw new XmlPullParserException("Incomplete range tag.", parser, null);
556             }
557
558             return new ColorRange(new Range<>(h[0], h[1]), new Range<>(s[0], s[1]),
559                     new Range<>(l[0], l[1]));
560         }
561
562         private void parsePalettes(XmlPullParser parser)
563                 throws XmlPullParserException, IOException {
564             parser.require(XmlPullParser.START_TAG, null, "palettes");
565             while (parser.next() != XmlPullParser.END_TAG) {
566                 if (parser.getEventType() != XmlPullParser.START_TAG) {
567                     continue;
568                 }
569                 String name = parser.getName();
570                 // Starts by looking for the entry tag
571                 if (name.equals("palette")) {
572                     mTonalPalettes.add(readPalette(parser));
573                     parser.next();
574                 } else {
575                     throw new XmlPullParserException("Invalid tag: " + name);
576                 }
577             }
578         }
579
580         private TonalPalette readPalette(XmlPullParser parser)
581                 throws XmlPullParserException, IOException {
582             parser.require(XmlPullParser.START_TAG, null, "palette");
583
584             float[] h = readFloatArray(parser.getAttributeValue(null, "h"));
585             float[] s = readFloatArray(parser.getAttributeValue(null, "s"));
586             float[] l = readFloatArray(parser.getAttributeValue(null, "l"));
587
588             if (h == null || s == null || l == null) {
589                 throw new XmlPullParserException("Incomplete range tag.", parser, null);
590             }
591
592             return new TonalPalette(h, s, l);
593         }
594
595         private float[] readFloatArray(String attributeValue)
596                 throws IOException, XmlPullParserException {
597             String[] tokens = attributeValue.replaceAll(" ", "").replaceAll("\n", "").split(",");
598             float[] numbers = new float[tokens.length];
599             for (int i = 0; i < tokens.length; i++) {
600                 numbers[i] = Float.parseFloat(tokens[i]);
601             }
602             return numbers;
603         }
604     }
605 }