Home | History | Annotate | Download | only in palette
      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.graphics.palette;
     18 
     19 import android.annotation.ColorInt;
     20 import android.annotation.NonNull;
     21 import android.annotation.Nullable;
     22 import android.graphics.Bitmap;
     23 import android.graphics.Color;
     24 import android.graphics.Rect;
     25 import android.os.AsyncTask;
     26 import android.util.ArrayMap;
     27 import android.util.Log;
     28 import android.util.SparseBooleanArray;
     29 import android.util.TimingLogger;
     30 
     31 import com.android.internal.graphics.ColorUtils;
     32 
     33 import java.util.ArrayList;
     34 import java.util.Arrays;
     35 import java.util.Collections;
     36 import java.util.List;
     37 import java.util.Map;
     38 
     39 
     40 /**
     41  * Copied from: /frameworks/support/v7/palette/src/main/java/android/support/v7/
     42  * graphics/Palette.java
     43  *
     44  * A helper class to extract prominent colors from an image.
     45  * <p>
     46  * A number of colors with different profiles are extracted from the image:
     47  * <ul>
     48  *     <li>Vibrant</li>
     49  *     <li>Vibrant Dark</li>
     50  *     <li>Vibrant Light</li>
     51  *     <li>Muted</li>
     52  *     <li>Muted Dark</li>
     53  *     <li>Muted Light</li>
     54  * </ul>
     55  * These can be retrieved from the appropriate getter method.
     56  *
     57  * <p>
     58  * Instances are created with a {@link Palette.Builder} which supports several options to tweak the
     59  * generated Palette. See that class' documentation for more information.
     60  * <p>
     61  * Generation should always be completed on a background thread, ideally the one in
     62  * which you load your image on. {@link Palette.Builder} supports both synchronous and asynchronous
     63  * generation:
     64  *
     65  * <pre>
     66  * // Synchronous
     67  * Palette p = Palette.from(bitmap).generate();
     68  *
     69  * // Asynchronous
     70  * Palette.from(bitmap).generate(new PaletteAsyncListener() {
     71  *     public void onGenerated(Palette p) {
     72  *         // Use generated instance
     73  *     }
     74  * });
     75  * </pre>
     76  */
     77 public final class Palette {
     78 
     79     /**
     80      * Listener to be used with {@link #generateAsync(Bitmap, Palette.PaletteAsyncListener)} or
     81      * {@link #generateAsync(Bitmap, int, Palette.PaletteAsyncListener)}
     82      */
     83     public interface PaletteAsyncListener {
     84 
     85         /**
     86          * Called when the {@link Palette} has been generated.
     87          */
     88         void onGenerated(Palette palette);
     89     }
     90 
     91     static final int DEFAULT_RESIZE_BITMAP_AREA = 112 * 112;
     92     static final int DEFAULT_CALCULATE_NUMBER_COLORS = 16;
     93 
     94     static final float MIN_CONTRAST_TITLE_TEXT = 3.0f;
     95     static final float MIN_CONTRAST_BODY_TEXT = 4.5f;
     96 
     97     static final String LOG_TAG = "Palette";
     98     static final boolean LOG_TIMINGS = false;
     99 
    100     /**
    101      * Start generating a {@link Palette} with the returned {@link Palette.Builder} instance.
    102      */
    103     public static Palette.Builder from(Bitmap bitmap) {
    104         return new Palette.Builder(bitmap);
    105     }
    106 
    107     /**
    108      * Generate a {@link Palette} from the pre-generated list of {@link Palette.Swatch} swatches.
    109      * This is useful for testing, or if you want to resurrect a {@link Palette} instance from a
    110      * list of swatches. Will return null if the {@code swatches} is null.
    111      */
    112     public static Palette from(List<Palette.Swatch> swatches) {
    113         return new Palette.Builder(swatches).generate();
    114     }
    115 
    116     /**
    117      * @deprecated Use {@link Palette.Builder} to generate the Palette.
    118      */
    119     @Deprecated
    120     public static Palette generate(Bitmap bitmap) {
    121         return from(bitmap).generate();
    122     }
    123 
    124     /**
    125      * @deprecated Use {@link Palette.Builder} to generate the Palette.
    126      */
    127     @Deprecated
    128     public static Palette generate(Bitmap bitmap, int numColors) {
    129         return from(bitmap).maximumColorCount(numColors).generate();
    130     }
    131 
    132     /**
    133      * @deprecated Use {@link Palette.Builder} to generate the Palette.
    134      */
    135     @Deprecated
    136     public static AsyncTask<Bitmap, Void, Palette> generateAsync(
    137             Bitmap bitmap, Palette.PaletteAsyncListener listener) {
    138         return from(bitmap).generate(listener);
    139     }
    140 
    141     /**
    142      * @deprecated Use {@link Palette.Builder} to generate the Palette.
    143      */
    144     @Deprecated
    145     public static AsyncTask<Bitmap, Void, Palette> generateAsync(
    146             final Bitmap bitmap, final int numColors, final Palette.PaletteAsyncListener listener) {
    147         return from(bitmap).maximumColorCount(numColors).generate(listener);
    148     }
    149 
    150     private final List<Palette.Swatch> mSwatches;
    151     private final List<Target> mTargets;
    152 
    153     private final Map<Target, Palette.Swatch> mSelectedSwatches;
    154     private final SparseBooleanArray mUsedColors;
    155 
    156     private final Palette.Swatch mDominantSwatch;
    157 
    158     Palette(List<Palette.Swatch> swatches, List<Target> targets) {
    159         mSwatches = swatches;
    160         mTargets = targets;
    161 
    162         mUsedColors = new SparseBooleanArray();
    163         mSelectedSwatches = new ArrayMap<>();
    164 
    165         mDominantSwatch = findDominantSwatch();
    166     }
    167 
    168     /**
    169      * Returns all of the swatches which make up the palette.
    170      */
    171     @NonNull
    172     public List<Palette.Swatch> getSwatches() {
    173         return Collections.unmodifiableList(mSwatches);
    174     }
    175 
    176     /**
    177      * Returns the targets used to generate this palette.
    178      */
    179     @NonNull
    180     public List<Target> getTargets() {
    181         return Collections.unmodifiableList(mTargets);
    182     }
    183 
    184     /**
    185      * Returns the most vibrant swatch in the palette. Might be null.
    186      *
    187      * @see Target#VIBRANT
    188      */
    189     @Nullable
    190     public Palette.Swatch getVibrantSwatch() {
    191         return getSwatchForTarget(Target.VIBRANT);
    192     }
    193 
    194     /**
    195      * Returns a light and vibrant swatch from the palette. Might be null.
    196      *
    197      * @see Target#LIGHT_VIBRANT
    198      */
    199     @Nullable
    200     public Palette.Swatch getLightVibrantSwatch() {
    201         return getSwatchForTarget(Target.LIGHT_VIBRANT);
    202     }
    203 
    204     /**
    205      * Returns a dark and vibrant swatch from the palette. Might be null.
    206      *
    207      * @see Target#DARK_VIBRANT
    208      */
    209     @Nullable
    210     public Palette.Swatch getDarkVibrantSwatch() {
    211         return getSwatchForTarget(Target.DARK_VIBRANT);
    212     }
    213 
    214     /**
    215      * Returns a muted swatch from the palette. Might be null.
    216      *
    217      * @see Target#MUTED
    218      */
    219     @Nullable
    220     public Palette.Swatch getMutedSwatch() {
    221         return getSwatchForTarget(Target.MUTED);
    222     }
    223 
    224     /**
    225      * Returns a muted and light swatch from the palette. Might be null.
    226      *
    227      * @see Target#LIGHT_MUTED
    228      */
    229     @Nullable
    230     public Palette.Swatch getLightMutedSwatch() {
    231         return getSwatchForTarget(Target.LIGHT_MUTED);
    232     }
    233 
    234     /**
    235      * Returns a muted and dark swatch from the palette. Might be null.
    236      *
    237      * @see Target#DARK_MUTED
    238      */
    239     @Nullable
    240     public Palette.Swatch getDarkMutedSwatch() {
    241         return getSwatchForTarget(Target.DARK_MUTED);
    242     }
    243 
    244     /**
    245      * Returns the most vibrant color in the palette as an RGB packed int.
    246      *
    247      * @param defaultColor value to return if the swatch isn't available
    248      * @see #getVibrantSwatch()
    249      */
    250     @ColorInt
    251     public int getVibrantColor(@ColorInt final int defaultColor) {
    252         return getColorForTarget(Target.VIBRANT, defaultColor);
    253     }
    254 
    255     /**
    256      * Returns a light and vibrant color from the palette as an RGB packed int.
    257      *
    258      * @param defaultColor value to return if the swatch isn't available
    259      * @see #getLightVibrantSwatch()
    260      */
    261     @ColorInt
    262     public int getLightVibrantColor(@ColorInt final int defaultColor) {
    263         return getColorForTarget(Target.LIGHT_VIBRANT, defaultColor);
    264     }
    265 
    266     /**
    267      * Returns a dark and vibrant color from the palette as an RGB packed int.
    268      *
    269      * @param defaultColor value to return if the swatch isn't available
    270      * @see #getDarkVibrantSwatch()
    271      */
    272     @ColorInt
    273     public int getDarkVibrantColor(@ColorInt final int defaultColor) {
    274         return getColorForTarget(Target.DARK_VIBRANT, defaultColor);
    275     }
    276 
    277     /**
    278      * Returns a muted color from the palette as an RGB packed int.
    279      *
    280      * @param defaultColor value to return if the swatch isn't available
    281      * @see #getMutedSwatch()
    282      */
    283     @ColorInt
    284     public int getMutedColor(@ColorInt final int defaultColor) {
    285         return getColorForTarget(Target.MUTED, defaultColor);
    286     }
    287 
    288     /**
    289      * Returns a muted and light color from the palette as an RGB packed int.
    290      *
    291      * @param defaultColor value to return if the swatch isn't available
    292      * @see #getLightMutedSwatch()
    293      */
    294     @ColorInt
    295     public int getLightMutedColor(@ColorInt final int defaultColor) {
    296         return getColorForTarget(Target.LIGHT_MUTED, defaultColor);
    297     }
    298 
    299     /**
    300      * Returns a muted and dark color from the palette as an RGB packed int.
    301      *
    302      * @param defaultColor value to return if the swatch isn't available
    303      * @see #getDarkMutedSwatch()
    304      */
    305     @ColorInt
    306     public int getDarkMutedColor(@ColorInt final int defaultColor) {
    307         return getColorForTarget(Target.DARK_MUTED, defaultColor);
    308     }
    309 
    310     /**
    311      * Returns the selected swatch for the given target from the palette, or {@code null} if one
    312      * could not be found.
    313      */
    314     @Nullable
    315     public Palette.Swatch getSwatchForTarget(@NonNull final Target target) {
    316         return mSelectedSwatches.get(target);
    317     }
    318 
    319     /**
    320      * Returns the selected color for the given target from the palette as an RGB packed int.
    321      *
    322      * @param defaultColor value to return if the swatch isn't available
    323      */
    324     @ColorInt
    325     public int getColorForTarget(@NonNull final Target target, @ColorInt final int defaultColor) {
    326         Palette.Swatch swatch = getSwatchForTarget(target);
    327         return swatch != null ? swatch.getRgb() : defaultColor;
    328     }
    329 
    330     /**
    331      * Returns the dominant swatch from the palette.
    332      *
    333      * <p>The dominant swatch is defined as the swatch with the greatest population (frequency)
    334      * within the palette.</p>
    335      */
    336     @Nullable
    337     public Palette.Swatch getDominantSwatch() {
    338         return mDominantSwatch;
    339     }
    340 
    341     /**
    342      * Returns the color of the dominant swatch from the palette, as an RGB packed int.
    343      *
    344      * @param defaultColor value to return if the swatch isn't available
    345      * @see #getDominantSwatch()
    346      */
    347     @ColorInt
    348     public int getDominantColor(@ColorInt int defaultColor) {
    349         return mDominantSwatch != null ? mDominantSwatch.getRgb() : defaultColor;
    350     }
    351 
    352     void generate() {
    353         // We need to make sure that the scored targets are generated first. This is so that
    354         // inherited targets have something to inherit from
    355         for (int i = 0, count = mTargets.size(); i < count; i++) {
    356             final Target target = mTargets.get(i);
    357             target.normalizeWeights();
    358             mSelectedSwatches.put(target, generateScoredTarget(target));
    359         }
    360         // We now clear out the used colors
    361         mUsedColors.clear();
    362     }
    363 
    364     private Palette.Swatch generateScoredTarget(final Target target) {
    365         final Palette.Swatch maxScoreSwatch = getMaxScoredSwatchForTarget(target);
    366         if (maxScoreSwatch != null && target.isExclusive()) {
    367             // If we have a swatch, and the target is exclusive, add the color to the used list
    368             mUsedColors.append(maxScoreSwatch.getRgb(), true);
    369         }
    370         return maxScoreSwatch;
    371     }
    372 
    373     private Palette.Swatch getMaxScoredSwatchForTarget(final Target target) {
    374         float maxScore = 0;
    375         Palette.Swatch maxScoreSwatch = null;
    376         for (int i = 0, count = mSwatches.size(); i < count; i++) {
    377             final Palette.Swatch swatch = mSwatches.get(i);
    378             if (shouldBeScoredForTarget(swatch, target)) {
    379                 final float score = generateScore(swatch, target);
    380                 if (maxScoreSwatch == null || score > maxScore) {
    381                     maxScoreSwatch = swatch;
    382                     maxScore = score;
    383                 }
    384             }
    385         }
    386         return maxScoreSwatch;
    387     }
    388 
    389     private boolean shouldBeScoredForTarget(final Palette.Swatch swatch, final Target target) {
    390         // Check whether the HSL values are within the correct ranges, and this color hasn't
    391         // been used yet.
    392         final float hsl[] = swatch.getHsl();
    393         return hsl[1] >= target.getMinimumSaturation() && hsl[1] <= target.getMaximumSaturation()
    394                 && hsl[2] >= target.getMinimumLightness() && hsl[2] <= target.getMaximumLightness()
    395                 && !mUsedColors.get(swatch.getRgb());
    396     }
    397 
    398     private float generateScore(Palette.Swatch swatch, Target target) {
    399         final float[] hsl = swatch.getHsl();
    400 
    401         float saturationScore = 0;
    402         float luminanceScore = 0;
    403         float populationScore = 0;
    404 
    405         final int maxPopulation = mDominantSwatch != null ? mDominantSwatch.getPopulation() : 1;
    406 
    407         if (target.getSaturationWeight() > 0) {
    408             saturationScore = target.getSaturationWeight()
    409                     * (1f - Math.abs(hsl[1] - target.getTargetSaturation()));
    410         }
    411         if (target.getLightnessWeight() > 0) {
    412             luminanceScore = target.getLightnessWeight()
    413                     * (1f - Math.abs(hsl[2] - target.getTargetLightness()));
    414         }
    415         if (target.getPopulationWeight() > 0) {
    416             populationScore = target.getPopulationWeight()
    417                     * (swatch.getPopulation() / (float) maxPopulation);
    418         }
    419 
    420         return saturationScore + luminanceScore + populationScore;
    421     }
    422 
    423     private Palette.Swatch findDominantSwatch() {
    424         int maxPop = Integer.MIN_VALUE;
    425         Palette.Swatch maxSwatch = null;
    426         for (int i = 0, count = mSwatches.size(); i < count; i++) {
    427             Palette.Swatch swatch = mSwatches.get(i);
    428             if (swatch.getPopulation() > maxPop) {
    429                 maxSwatch = swatch;
    430                 maxPop = swatch.getPopulation();
    431             }
    432         }
    433         return maxSwatch;
    434     }
    435 
    436     private static float[] copyHslValues(Palette.Swatch color) {
    437         final float[] newHsl = new float[3];
    438         System.arraycopy(color.getHsl(), 0, newHsl, 0, 3);
    439         return newHsl;
    440     }
    441 
    442     /**
    443      * Represents a color swatch generated from an image's palette. The RGB color can be retrieved
    444      * by calling {@link #getRgb()}.
    445      */
    446     public static final class Swatch {
    447         private final int mRed, mGreen, mBlue;
    448         private final int mRgb;
    449         private final int mPopulation;
    450 
    451         private boolean mGeneratedTextColors;
    452         private int mTitleTextColor;
    453         private int mBodyTextColor;
    454 
    455         private float[] mHsl;
    456 
    457         public Swatch(@ColorInt int color, int population) {
    458             mRed = Color.red(color);
    459             mGreen = Color.green(color);
    460             mBlue = Color.blue(color);
    461             mRgb = color;
    462             mPopulation = population;
    463         }
    464 
    465         Swatch(int red, int green, int blue, int population) {
    466             mRed = red;
    467             mGreen = green;
    468             mBlue = blue;
    469             mRgb = Color.rgb(red, green, blue);
    470             mPopulation = population;
    471         }
    472 
    473         Swatch(float[] hsl, int population) {
    474             this(ColorUtils.HSLToColor(hsl), population);
    475             mHsl = hsl;
    476         }
    477 
    478         /**
    479          * @return this swatch's RGB color value
    480          */
    481         @ColorInt
    482         public int getRgb() {
    483             return mRgb;
    484         }
    485 
    486         /**
    487          * Return this swatch's HSL values.
    488          *     hsv[0] is Hue [0 .. 360)
    489          *     hsv[1] is Saturation [0...1]
    490          *     hsv[2] is Lightness [0...1]
    491          */
    492         public float[] getHsl() {
    493             if (mHsl == null) {
    494                 mHsl = new float[3];
    495             }
    496             ColorUtils.RGBToHSL(mRed, mGreen, mBlue, mHsl);
    497             return mHsl;
    498         }
    499 
    500         /**
    501          * @return the number of pixels represented by this swatch
    502          */
    503         public int getPopulation() {
    504             return mPopulation;
    505         }
    506 
    507         /**
    508          * Returns an appropriate color to use for any 'title' text which is displayed over this
    509          * {@link Palette.Swatch}'s color. This color is guaranteed to have sufficient contrast.
    510          */
    511         @ColorInt
    512         public int getTitleTextColor() {
    513             ensureTextColorsGenerated();
    514             return mTitleTextColor;
    515         }
    516 
    517         /**
    518          * Returns an appropriate color to use for any 'body' text which is displayed over this
    519          * {@link Palette.Swatch}'s color. This color is guaranteed to have sufficient contrast.
    520          */
    521         @ColorInt
    522         public int getBodyTextColor() {
    523             ensureTextColorsGenerated();
    524             return mBodyTextColor;
    525         }
    526 
    527         private void ensureTextColorsGenerated() {
    528             if (!mGeneratedTextColors) {
    529                 // First check white, as most colors will be dark
    530                 final int lightBodyAlpha = ColorUtils.calculateMinimumAlpha(
    531                         Color.WHITE, mRgb, MIN_CONTRAST_BODY_TEXT);
    532                 final int lightTitleAlpha = ColorUtils.calculateMinimumAlpha(
    533                         Color.WHITE, mRgb, MIN_CONTRAST_TITLE_TEXT);
    534 
    535                 if (lightBodyAlpha != -1 && lightTitleAlpha != -1) {
    536                     // If we found valid light values, use them and return
    537                     mBodyTextColor = ColorUtils.setAlphaComponent(Color.WHITE, lightBodyAlpha);
    538                     mTitleTextColor = ColorUtils.setAlphaComponent(Color.WHITE, lightTitleAlpha);
    539                     mGeneratedTextColors = true;
    540                     return;
    541                 }
    542 
    543                 final int darkBodyAlpha = ColorUtils.calculateMinimumAlpha(
    544                         Color.BLACK, mRgb, MIN_CONTRAST_BODY_TEXT);
    545                 final int darkTitleAlpha = ColorUtils.calculateMinimumAlpha(
    546                         Color.BLACK, mRgb, MIN_CONTRAST_TITLE_TEXT);
    547 
    548                 if (darkBodyAlpha != -1 && darkTitleAlpha != -1) {
    549                     // If we found valid dark values, use them and return
    550                     mBodyTextColor = ColorUtils.setAlphaComponent(Color.BLACK, darkBodyAlpha);
    551                     mTitleTextColor = ColorUtils.setAlphaComponent(Color.BLACK, darkTitleAlpha);
    552                     mGeneratedTextColors = true;
    553                     return;
    554                 }
    555 
    556                 // If we reach here then we can not find title and body values which use the same
    557                 // lightness, we need to use mismatched values
    558                 mBodyTextColor = lightBodyAlpha != -1
    559                         ? ColorUtils.setAlphaComponent(Color.WHITE, lightBodyAlpha)
    560                         : ColorUtils.setAlphaComponent(Color.BLACK, darkBodyAlpha);
    561                 mTitleTextColor = lightTitleAlpha != -1
    562                         ? ColorUtils.setAlphaComponent(Color.WHITE, lightTitleAlpha)
    563                         : ColorUtils.setAlphaComponent(Color.BLACK, darkTitleAlpha);
    564                 mGeneratedTextColors = true;
    565             }
    566         }
    567 
    568         @Override
    569         public String toString() {
    570             return new StringBuilder(getClass().getSimpleName())
    571                     .append(" [RGB: #").append(Integer.toHexString(getRgb())).append(']')
    572                     .append(" [HSL: ").append(Arrays.toString(getHsl())).append(']')
    573                     .append(" [Population: ").append(mPopulation).append(']')
    574                     .append(" [Title Text: #").append(Integer.toHexString(getTitleTextColor()))
    575                     .append(']')
    576                     .append(" [Body Text: #").append(Integer.toHexString(getBodyTextColor()))
    577                     .append(']').toString();
    578         }
    579 
    580         @Override
    581         public boolean equals(Object o) {
    582             if (this == o) {
    583                 return true;
    584             }
    585             if (o == null || getClass() != o.getClass()) {
    586                 return false;
    587             }
    588 
    589             Palette.Swatch
    590                     swatch = (Palette.Swatch) o;
    591             return mPopulation == swatch.mPopulation && mRgb == swatch.mRgb;
    592         }
    593 
    594         @Override
    595         public int hashCode() {
    596             return 31 * mRgb + mPopulation;
    597         }
    598     }
    599 
    600     /**
    601      * Builder class for generating {@link Palette} instances.
    602      */
    603     public static final class Builder {
    604         private final List<Palette.Swatch> mSwatches;
    605         private final Bitmap mBitmap;
    606 
    607         private final List<Target> mTargets = new ArrayList<>();
    608 
    609         private int mMaxColors = DEFAULT_CALCULATE_NUMBER_COLORS;
    610         private int mResizeArea = DEFAULT_RESIZE_BITMAP_AREA;
    611         private int mResizeMaxDimension = -1;
    612 
    613         private final List<Palette.Filter> mFilters = new ArrayList<>();
    614         private Rect mRegion;
    615 
    616         private Quantizer mQuantizer;
    617 
    618         /**
    619          * Construct a new {@link Palette.Builder} using a source {@link Bitmap}
    620          */
    621         public Builder(Bitmap bitmap) {
    622             if (bitmap == null || bitmap.isRecycled()) {
    623                 throw new IllegalArgumentException("Bitmap is not valid");
    624             }
    625             mFilters.add(DEFAULT_FILTER);
    626             mBitmap = bitmap;
    627             mSwatches = null;
    628 
    629             // Add the default targets
    630             mTargets.add(Target.LIGHT_VIBRANT);
    631             mTargets.add(Target.VIBRANT);
    632             mTargets.add(Target.DARK_VIBRANT);
    633             mTargets.add(Target.LIGHT_MUTED);
    634             mTargets.add(Target.MUTED);
    635             mTargets.add(Target.DARK_MUTED);
    636         }
    637 
    638         /**
    639          * Construct a new {@link Palette.Builder} using a list of {@link Palette.Swatch} instances.
    640          * Typically only used for testing.
    641          */
    642         public Builder(List<Palette.Swatch> swatches) {
    643             if (swatches == null || swatches.isEmpty()) {
    644                 throw new IllegalArgumentException("List of Swatches is not valid");
    645             }
    646             mFilters.add(DEFAULT_FILTER);
    647             mSwatches = swatches;
    648             mBitmap = null;
    649         }
    650 
    651         /**
    652          * Set the maximum number of colors to use in the quantization step when using a
    653          * {@link android.graphics.Bitmap} as the source.
    654          * <p>
    655          * Good values for depend on the source image type. For landscapes, good values are in
    656          * the range 10-16. For images which are largely made up of people's faces then this
    657          * value should be increased to ~24.
    658          */
    659         @NonNull
    660         public Palette.Builder maximumColorCount(int colors) {
    661             mMaxColors = colors;
    662             return this;
    663         }
    664 
    665         /**
    666          * Set the resize value when using a {@link android.graphics.Bitmap} as the source.
    667          * If the bitmap's largest dimension is greater than the value specified, then the bitmap
    668          * will be resized so that its largest dimension matches {@code maxDimension}. If the
    669          * bitmap is smaller or equal, the original is used as-is.
    670          *
    671          * @deprecated Using {@link #resizeBitmapArea(int)} is preferred since it can handle
    672          * abnormal aspect ratios more gracefully.
    673          *
    674          * @param maxDimension the number of pixels that the max dimension should be scaled down to,
    675          *                     or any value <= 0 to disable resizing.
    676          */
    677         @NonNull
    678         @Deprecated
    679         public Palette.Builder resizeBitmapSize(final int maxDimension) {
    680             mResizeMaxDimension = maxDimension;
    681             mResizeArea = -1;
    682             return this;
    683         }
    684 
    685         /**
    686          * Set the resize value when using a {@link android.graphics.Bitmap} as the source.
    687          * If the bitmap's area is greater than the value specified, then the bitmap
    688          * will be resized so that its area matches {@code area}. If the
    689          * bitmap is smaller or equal, the original is used as-is.
    690          * <p>
    691          * This value has a large effect on the processing time. The larger the resized image is,
    692          * the greater time it will take to generate the palette. The smaller the image is, the
    693          * more detail is lost in the resulting image and thus less precision for color selection.
    694          *
    695          * @param area the number of pixels that the intermediary scaled down Bitmap should cover,
    696          *             or any value <= 0 to disable resizing.
    697          */
    698         @NonNull
    699         public Palette.Builder resizeBitmapArea(final int area) {
    700             mResizeArea = area;
    701             mResizeMaxDimension = -1;
    702             return this;
    703         }
    704 
    705         /**
    706          * Clear all added filters. This includes any default filters added automatically by
    707          * {@link Palette}.
    708          */
    709         @NonNull
    710         public Palette.Builder clearFilters() {
    711             mFilters.clear();
    712             return this;
    713         }
    714 
    715         /**
    716          * Add a filter to be able to have fine grained control over which colors are
    717          * allowed in the resulting palette.
    718          *
    719          * @param filter filter to add.
    720          */
    721         @NonNull
    722         public Palette.Builder addFilter(
    723                 Palette.Filter filter) {
    724             if (filter != null) {
    725                 mFilters.add(filter);
    726             }
    727             return this;
    728         }
    729 
    730         /**
    731          * Set a specific quantization algorithm. {@link ColorCutQuantizer} will
    732          * be used if unspecified.
    733          *
    734          * @param quantizer Quantizer implementation.
    735          */
    736         @NonNull
    737         public Palette.Builder setQuantizer(Quantizer quantizer) {
    738             mQuantizer = quantizer;
    739             return this;
    740         }
    741 
    742         /**
    743          * Set a region of the bitmap to be used exclusively when calculating the palette.
    744          * <p>This only works when the original input is a {@link Bitmap}.</p>
    745          *
    746          * @param left The left side of the rectangle used for the region.
    747          * @param top The top of the rectangle used for the region.
    748          * @param right The right side of the rectangle used for the region.
    749          * @param bottom The bottom of the rectangle used for the region.
    750          */
    751         @NonNull
    752         public Palette.Builder setRegion(int left, int top, int right, int bottom) {
    753             if (mBitmap != null) {
    754                 if (mRegion == null) mRegion = new Rect();
    755                 // Set the Rect to be initially the whole Bitmap
    756                 mRegion.set(0, 0, mBitmap.getWidth(), mBitmap.getHeight());
    757                 // Now just get the intersection with the region
    758                 if (!mRegion.intersect(left, top, right, bottom)) {
    759                     throw new IllegalArgumentException("The given region must intersect with "
    760                             + "the Bitmap's dimensions.");
    761                 }
    762             }
    763             return this;
    764         }
    765 
    766         /**
    767          * Clear any previously region set via {@link #setRegion(int, int, int, int)}.
    768          */
    769         @NonNull
    770         public Palette.Builder clearRegion() {
    771             mRegion = null;
    772             return this;
    773         }
    774 
    775         /**
    776          * Add a target profile to be generated in the palette.
    777          *
    778          * <p>You can retrieve the result via {@link Palette#getSwatchForTarget(Target)}.</p>
    779          */
    780         @NonNull
    781         public Palette.Builder addTarget(@NonNull final Target target) {
    782             if (!mTargets.contains(target)) {
    783                 mTargets.add(target);
    784             }
    785             return this;
    786         }
    787 
    788         /**
    789          * Clear all added targets. This includes any default targets added automatically by
    790          * {@link Palette}.
    791          */
    792         @NonNull
    793         public Palette.Builder clearTargets() {
    794             if (mTargets != null) {
    795                 mTargets.clear();
    796             }
    797             return this;
    798         }
    799 
    800         /**
    801          * Generate and return the {@link Palette} synchronously.
    802          */
    803         @NonNull
    804         public Palette generate() {
    805             final TimingLogger logger = LOG_TIMINGS
    806                     ? new TimingLogger(LOG_TAG, "Generation")
    807                     : null;
    808 
    809             List<Palette.Swatch> swatches;
    810 
    811             if (mBitmap != null) {
    812                 // We have a Bitmap so we need to use quantization to reduce the number of colors
    813 
    814                 // First we'll scale down the bitmap if needed
    815                 final Bitmap bitmap = scaleBitmapDown(mBitmap);
    816 
    817                 if (logger != null) {
    818                     logger.addSplit("Processed Bitmap");
    819                 }
    820 
    821                 final Rect region = mRegion;
    822                 if (bitmap != mBitmap && region != null) {
    823                     // If we have a scaled bitmap and a selected region, we need to scale down the
    824                     // region to match the new scale
    825                     final double scale = bitmap.getWidth() / (double) mBitmap.getWidth();
    826                     region.left = (int) Math.floor(region.left * scale);
    827                     region.top = (int) Math.floor(region.top * scale);
    828                     region.right = Math.min((int) Math.ceil(region.right * scale),
    829                             bitmap.getWidth());
    830                     region.bottom = Math.min((int) Math.ceil(region.bottom * scale),
    831                             bitmap.getHeight());
    832                 }
    833 
    834                 // Now generate a quantizer from the Bitmap
    835                 if (mQuantizer == null) {
    836                     mQuantizer = new ColorCutQuantizer();
    837                 }
    838                 mQuantizer.quantize(getPixelsFromBitmap(bitmap),
    839                             mMaxColors, mFilters.isEmpty() ? null :
    840                             mFilters.toArray(new Palette.Filter[mFilters.size()]));
    841 
    842                 // If created a new bitmap, recycle it
    843                 if (bitmap != mBitmap) {
    844                     bitmap.recycle();
    845                 }
    846 
    847                 swatches = mQuantizer.getQuantizedColors();
    848 
    849                 if (logger != null) {
    850                     logger.addSplit("Color quantization completed");
    851                 }
    852             } else {
    853                 // Else we're using the provided swatches
    854                 swatches = mSwatches;
    855             }
    856 
    857             // Now create a Palette instance
    858             final Palette p = new Palette(swatches, mTargets);
    859             // And make it generate itself
    860             p.generate();
    861 
    862             if (logger != null) {
    863                 logger.addSplit("Created Palette");
    864                 logger.dumpToLog();
    865             }
    866 
    867             return p;
    868         }
    869 
    870         /**
    871          * Generate the {@link Palette} asynchronously. The provided listener's
    872          * {@link Palette.PaletteAsyncListener#onGenerated} method will be called with the palette when
    873          * generated.
    874          */
    875         @NonNull
    876         public AsyncTask<Bitmap, Void, Palette> generate(final Palette.PaletteAsyncListener listener) {
    877             if (listener == null) {
    878                 throw new IllegalArgumentException("listener can not be null");
    879             }
    880 
    881             return new AsyncTask<Bitmap, Void, Palette>() {
    882                 @Override
    883                 protected Palette doInBackground(Bitmap... params) {
    884                     try {
    885                         return generate();
    886                     } catch (Exception e) {
    887                         Log.e(LOG_TAG, "Exception thrown during async generate", e);
    888                         return null;
    889                     }
    890                 }
    891 
    892                 @Override
    893                 protected void onPostExecute(Palette colorExtractor) {
    894                     listener.onGenerated(colorExtractor);
    895                 }
    896             }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, mBitmap);
    897         }
    898 
    899         private int[] getPixelsFromBitmap(Bitmap bitmap) {
    900             final int bitmapWidth = bitmap.getWidth();
    901             final int bitmapHeight = bitmap.getHeight();
    902             final int[] pixels = new int[bitmapWidth * bitmapHeight];
    903             bitmap.getPixels(pixels, 0, bitmapWidth, 0, 0, bitmapWidth, bitmapHeight);
    904 
    905             if (mRegion == null) {
    906                 // If we don't have a region, return all of the pixels
    907                 return pixels;
    908             } else {
    909                 // If we do have a region, lets create a subset array containing only the region's
    910                 // pixels
    911                 final int regionWidth = mRegion.width();
    912                 final int regionHeight = mRegion.height();
    913                 // pixels contains all of the pixels, so we need to iterate through each row and
    914                 // copy the regions pixels into a new smaller array
    915                 final int[] subsetPixels = new int[regionWidth * regionHeight];
    916                 for (int row = 0; row < regionHeight; row++) {
    917                     System.arraycopy(pixels, ((row + mRegion.top) * bitmapWidth) + mRegion.left,
    918                             subsetPixels, row * regionWidth, regionWidth);
    919                 }
    920                 return subsetPixels;
    921             }
    922         }
    923 
    924         /**
    925          * Scale the bitmap down as needed.
    926          */
    927         private Bitmap scaleBitmapDown(final Bitmap bitmap) {
    928             double scaleRatio = -1;
    929 
    930             if (mResizeArea > 0) {
    931                 final int bitmapArea = bitmap.getWidth() * bitmap.getHeight();
    932                 if (bitmapArea > mResizeArea) {
    933                     scaleRatio = Math.sqrt(mResizeArea / (double) bitmapArea);
    934                 }
    935             } else if (mResizeMaxDimension > 0) {
    936                 final int maxDimension = Math.max(bitmap.getWidth(), bitmap.getHeight());
    937                 if (maxDimension > mResizeMaxDimension) {
    938                     scaleRatio = mResizeMaxDimension / (double) maxDimension;
    939                 }
    940             }
    941 
    942             if (scaleRatio <= 0) {
    943                 // Scaling has been disabled or not needed so just return the Bitmap
    944                 return bitmap;
    945             }
    946 
    947             return Bitmap.createScaledBitmap(bitmap,
    948                     (int) Math.ceil(bitmap.getWidth() * scaleRatio),
    949                     (int) Math.ceil(bitmap.getHeight() * scaleRatio),
    950                     false);
    951         }
    952     }
    953 
    954     /**
    955      * A Filter provides a mechanism for exercising fine-grained control over which colors
    956      * are valid within a resulting {@link Palette}.
    957      */
    958     public interface Filter {
    959         /**
    960          * Hook to allow clients to be able filter colors from resulting palette.
    961          *
    962          * @param rgb the color in RGB888.
    963          * @param hsl HSL representation of the color.
    964          *
    965          * @return true if the color is allowed, false if not.
    966          *
    967          * @see Palette.Builder#addFilter(Palette.Filter)
    968          */
    969         boolean isAllowed(int rgb, float[] hsl);
    970     }
    971 
    972     /**
    973      * The default filter.
    974      */
    975     static final Palette.Filter
    976             DEFAULT_FILTER = new Palette.Filter() {
    977         private static final float BLACK_MAX_LIGHTNESS = 0.05f;
    978         private static final float WHITE_MIN_LIGHTNESS = 0.95f;
    979 
    980         @Override
    981         public boolean isAllowed(int rgb, float[] hsl) {
    982             return !isWhite(hsl) && !isBlack(hsl) && !isNearRedILine(hsl);
    983         }
    984 
    985         /**
    986          * @return true if the color represents a color which is close to black.
    987          */
    988         private boolean isBlack(float[] hslColor) {
    989             return hslColor[2] <= BLACK_MAX_LIGHTNESS;
    990         }
    991 
    992         /**
    993          * @return true if the color represents a color which is close to white.
    994          */
    995         private boolean isWhite(float[] hslColor) {
    996             return hslColor[2] >= WHITE_MIN_LIGHTNESS;
    997         }
    998 
    999         /**
   1000          * @return true if the color lies close to the red side of the I line.
   1001          */
   1002         private boolean isNearRedILine(float[] hslColor) {
   1003             return hslColor[0] >= 10f && hslColor[0] <= 37f && hslColor[1] <= 0.82f;
   1004         }
   1005     };
   1006 }
   1007