001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.imaging.palette;
018
019import java.awt.color.ColorSpace;
020import java.awt.image.BufferedImage;
021import java.awt.image.ColorModel;
022import java.util.ArrayList;
023import java.util.Arrays;
024import java.util.HashSet;
025import java.util.List;
026import java.util.Set;
027import java.util.logging.Level;
028import java.util.logging.Logger;
029
030import org.apache.commons.imaging.ImageWriteException;
031
032/**
033 * Factory for creating palettes.
034 */
035public class PaletteFactory {
036
037    private static final Logger LOGGER = Logger.getLogger(PaletteFactory.class.getName());
038
039    public static final int COMPONENTS = 3; // in bits
040
041    /**
042     * Builds an exact complete opaque palette containing all the colors in {@code src},
043     * using an algorithm that is faster than {@linkplain #makeExactRgbPaletteSimple} for large images
044     * but uses 2 mebibytes of working memory. Treats all the colors as opaque.
045     * @param src the image whose palette to build
046     * @return the palette
047     */
048    public Palette makeExactRgbPaletteFancy(final BufferedImage src) {
049        // map what rgb values have been used
050
051        final byte[] rgbmap = new byte[256 * 256 * 32];
052
053        final int width = src.getWidth();
054        final int height = src.getHeight();
055
056        for (int y = 0; y < height; y++) {
057            for (int x = 0; x < width; x++) {
058                final int argb = src.getRGB(x, y);
059                final int rggbb = 0x1fffff & argb;
060                final int highred = 0x7 & (argb >> 21);
061                final int mask = 1 << highred;
062                rgbmap[rggbb] |= mask;
063            }
064        }
065
066        int count = 0;
067        for (final byte element : rgbmap) {
068            final int eight = 0xff & element;
069            count += Integer.bitCount(eight);
070        }
071
072        if (LOGGER.isLoggable(Level.FINEST)) {
073            LOGGER.finest("Used colors: " + count);
074        }
075
076        final int[] colormap = new int[count];
077        int mapsize = 0;
078        for (int i = 0; i < rgbmap.length; i++) {
079            final int eight = 0xff & rgbmap[i];
080            int mask = 0x80;
081            for (int j = 0; j < 8; j++) {
082                final int bit = eight & mask;
083                mask >>>= 1;
084
085                if (bit > 0) {
086                    final int rgb = i | ((7 - j) << 21);
087
088                    colormap[mapsize++] = rgb;
089                }
090            }
091        }
092
093        Arrays.sort(colormap);
094        return new SimplePalette(colormap);
095    }
096
097    private int pixelToQuantizationTableIndex(int argb, final int precision) {
098        int result = 0;
099        final int precisionMask = (1 << precision) - 1;
100
101        for (int i = 0; i < COMPONENTS; i++) {
102            int sample = argb & 0xff;
103            argb >>= 8;
104
105            sample >>= (8 - precision);
106            result = (result << precision) | (sample & precisionMask);
107        }
108
109        return result;
110    }
111
112    private int getFrequencyTotal(final int[] table, final int[] mins, final int[] maxs,
113            final int precision) {
114        int sum = 0;
115
116        for (int blue = mins[2]; blue <= maxs[2]; blue++) {
117            final int b = (blue << (2 * precision));
118            for (int green = mins[1]; green <= maxs[1]; green++) {
119                final int g = (green << (1 * precision));
120                for (int red = mins[0]; red <= maxs[0]; red++) {
121                    final int index = b | g | red;
122
123                    sum += table[index];
124                }
125            }
126        }
127
128        return sum;
129    }
130
131    private DivisionCandidate finishDivision(final ColorSpaceSubset subset,
132            final int component, final int precision, final int sum, final int slice) {
133        if (LOGGER.isLoggable(Level.FINEST)) {
134            subset.dump("trying (" + component + "): ");
135        }
136
137        final int total = subset.total;
138
139        if ((slice < subset.mins[component])
140                || (slice >= subset.maxs[component])) {
141            return null;
142        }
143
144        if ((sum < 1) || (sum >= total)) {
145            return null;
146        }
147
148        final int[] sliceMins = new int[subset.mins.length];
149        System.arraycopy(subset.mins, 0, sliceMins, 0, subset.mins.length);
150        final int[] sliceMaxs = new int[subset.maxs.length];
151        System.arraycopy(subset.maxs, 0, sliceMaxs, 0, subset.maxs.length);
152
153        sliceMaxs[component] = slice;
154        sliceMins[component] = slice + 1;
155
156        if (LOGGER.isLoggable(Level.FINEST)) {
157            LOGGER.finest("total: " + total);
158            LOGGER.finest("first total: " + sum);
159            LOGGER.finest("second total: " + (total - sum));
160            // System.out.println("start: " + start);
161            // System.out.println("end: " + end);
162            LOGGER.finest("slice: " + slice);
163
164        }
165
166        final ColorSpaceSubset first = new ColorSpaceSubset(sum, precision, subset.mins, sliceMaxs);
167        final ColorSpaceSubset second = new ColorSpaceSubset(total - sum, precision, sliceMins, subset.maxs);
168
169        return new DivisionCandidate(first, second);
170
171    }
172
173    private List<DivisionCandidate> divideSubset2(final int[] table,
174            final ColorSpaceSubset subset, final int component, final int precision) {
175        if (LOGGER.isLoggable(Level.FINEST)) {
176            subset.dump("trying (" + component + "): ");
177        }
178
179        final int total = subset.total;
180
181        final int[] sliceMins = new int[subset.mins.length];
182        System.arraycopy(subset.mins, 0, sliceMins, 0, subset.mins.length);
183        final int[] sliceMaxs = new int[subset.maxs.length];
184        System.arraycopy(subset.maxs, 0, sliceMaxs, 0, subset.maxs.length);
185
186        int sum1 = 0;
187        int slice1;
188        int last = 0;
189
190        for (slice1 = subset.mins[component]; slice1 != subset.maxs[component] + 1; slice1++) {
191            sliceMins[component] = slice1;
192            sliceMaxs[component] = slice1;
193
194            last = getFrequencyTotal(table, sliceMins, sliceMaxs, precision);
195
196            sum1 += last;
197
198            if (sum1 >= (total / 2)) {
199                break;
200            }
201        }
202
203        final int sum2 = sum1 - last;
204        final int slice2 = slice1 - 1;
205
206        final DivisionCandidate dc1 = finishDivision(subset, component, precision, sum1, slice1);
207        final DivisionCandidate dc2 = finishDivision(subset, component, precision, sum2, slice2);
208
209        final List<DivisionCandidate> result = new ArrayList<>();
210
211        if (dc1 != null) {
212            result.add(dc1);
213        }
214        if (dc2 != null) {
215            result.add(dc2);
216        }
217
218        return result;
219    }
220
221    private DivisionCandidate divideSubset2(final int[] table,
222            final ColorSpaceSubset subset, final int precision) {
223        final List<DivisionCandidate> dcs = new ArrayList<>();
224
225        dcs.addAll(divideSubset2(table, subset, 0, precision));
226        dcs.addAll(divideSubset2(table, subset, 1, precision));
227        dcs.addAll(divideSubset2(table, subset, 2, precision));
228
229        DivisionCandidate bestV = null;
230        double bestScore = Double.MAX_VALUE;
231
232        for (final DivisionCandidate dc : dcs) {
233            final ColorSpaceSubset first = dc.dst_a;
234            final ColorSpaceSubset second = dc.dst_b;
235            final int area1 = first.total;
236            final int area2 = second.total;
237
238            final int diff = Math.abs(area1 - area2);
239            final double score = ((double) diff) / ((double) Math.max(area1, area2));
240
241            if (bestV == null) {
242                bestV = dc;
243                bestScore = score;
244            } else if (score < bestScore) {
245                bestV = dc;
246                bestScore = score;
247            }
248
249        }
250
251        return bestV;
252    }
253
254    private static class DivisionCandidate {
255        // private final ColorSpaceSubset src;
256        private final ColorSpaceSubset dst_a;
257        private final ColorSpaceSubset dst_b;
258
259        DivisionCandidate(final ColorSpaceSubset dst_a, final ColorSpaceSubset dst_b) {
260            // this.src = src;
261            this.dst_a = dst_a;
262            this.dst_b = dst_b;
263        }
264    }
265
266    private void divide(final List<ColorSpaceSubset> v,
267                        final int desiredCount, final int[] table, final int precision) {
268        final List<ColorSpaceSubset> ignore = new ArrayList<>();
269
270        while (true) {
271            int maxArea = -1;
272            ColorSpaceSubset maxSubset = null;
273
274            for (final ColorSpaceSubset subset : v) {
275                if (ignore.contains(subset)) {
276                    continue;
277                }
278                final int area = subset.total;
279
280                if (maxSubset == null) {
281                    maxSubset = subset;
282                    maxArea = area;
283                } else if (area > maxArea) {
284                    maxSubset = subset;
285                    maxArea = area;
286                }
287            }
288
289            if (maxSubset == null) {
290                return;
291            }
292            if (LOGGER.isLoggable(Level.FINEST)) {
293                LOGGER.finest("\t" + "area: " + maxArea);
294            }
295
296            final DivisionCandidate dc = divideSubset2(table, maxSubset,
297                    precision);
298            if (dc != null) {
299                v.remove(maxSubset);
300                v.add(dc.dst_a);
301                v.add(dc.dst_b);
302            } else {
303                ignore.add(maxSubset);
304            }
305
306            if (v.size() == desiredCount) {
307                return;
308            }
309        }
310    }
311
312    /**
313     * Builds an inexact opaque palette of at most {@code max} colors in {@code src}
314     * using a variation of the Median Cut algorithm. Accurate to 6 bits per component,
315     * and works by splitting the color bounding box most heavily populated by colors
316     * along the component which splits the colors in that box most evenly.
317     * @param src the image whose palette to build
318     * @param max the maximum number of colors the palette can contain
319     * @return the palette of at most {@code max} colors
320     */
321    public Palette makeQuantizedRgbPalette(final BufferedImage src, final int max) {
322        final int precision = 6; // in bits
323
324        final int tableScale = precision * COMPONENTS;
325        final int tableSize = 1 << tableScale;
326        final int[] table = new int[tableSize];
327
328        final int width = src.getWidth();
329        final int height = src.getHeight();
330
331        List<ColorSpaceSubset> subsets = new ArrayList<>();
332        final ColorSpaceSubset all = new ColorSpaceSubset(width * height, precision);
333        subsets.add(all);
334
335        if (LOGGER.isLoggable(Level.FINEST)) {
336            final int preTotal = getFrequencyTotal(table, all.mins, all.maxs, precision);
337            LOGGER.finest("pre total: " + preTotal);
338        }
339
340        // step 1: count frequency of colors
341        for (int y = 0; y < height; y++) {
342            for (int x = 0; x < width; x++) {
343                final int argb = src.getRGB(x, y);
344
345                final int index = pixelToQuantizationTableIndex(argb, precision);
346
347                table[index]++;
348            }
349        }
350
351        if (LOGGER.isLoggable(Level.FINEST)) {
352            final int allTotal = getFrequencyTotal(table, all.mins, all.maxs, precision);
353            LOGGER.finest("all total: " + allTotal);
354            LOGGER.finest("width * height: " + (width * height));
355        }
356
357        divide(subsets, max, table, precision);
358
359        if (LOGGER.isLoggable(Level.FINEST)) {
360            LOGGER.finest("subsets: " + subsets.size());
361            LOGGER.finest("width*height: " + width * height);
362        }
363
364        for (int i = 0; i < subsets.size(); i++) {
365            final ColorSpaceSubset subset = subsets.get(i);
366
367            subset.setAverageRGB(table);
368
369            if (LOGGER.isLoggable(Level.FINEST)) {
370                subset.dump(i + ": ");
371            }
372        }
373
374        subsets.sort(ColorSpaceSubset.RGB_COMPARATOR);
375
376        return new QuantizedPalette(subsets, precision);
377    }
378
379    /**
380     * Builds an inexact possibly translucent palette of at most {@code max} colors in {@code src}
381     * using the traditional Median Cut algorithm. Color bounding boxes are split along the
382     * longest axis, with each step splitting the box. All bits in each component are used.
383     * The Algorithm is slower and seems exact than {@linkplain #makeQuantizedRgbPalette(BufferedImage, int)}.
384     * @param src the image whose palette to build
385     * @param transparent whether to consider the alpha values
386     * @param max the maximum number of colors the palette can contain
387     * @return the palette of at most {@code max} colors
388     * @throws ImageWriteException if it fails to process the palette
389     */
390    public Palette makeQuantizedRgbaPalette(final BufferedImage src, final boolean transparent, final int max) throws ImageWriteException {
391        return new MedianCutQuantizer(!transparent).process(src, max,
392                new LongestAxisMedianCut());
393    }
394
395    /**
396     * Builds an exact complete opaque palette containing all the colors in {@code src},
397     * and fails by returning {@code null} if there are more than {@code max} colors necessary to do this.
398     * @param src the image whose palette to build
399     * @param max the maximum number of colors the palette can contain
400     * @return the complete palette of {@code max} or less colors, or {@code null} if more than {@code max} colors are necessary
401     */
402    public SimplePalette makeExactRgbPaletteSimple(final BufferedImage src, final int max) {
403        // This is not efficient for large values of max, say, max > 256;
404        final Set<Integer> rgbs = new HashSet<>();
405
406        final int width = src.getWidth();
407        final int height = src.getHeight();
408
409        for (int y = 0; y < height; y++) {
410            for (int x = 0; x < width; x++) {
411                final int argb = src.getRGB(x, y);
412                final int rgb = 0xffffff & argb;
413
414                if (rgbs.add(rgb) && rgbs.size() > max) {
415                    return null;
416                }
417            }
418        }
419
420        final int[] result = new int[rgbs.size()];
421        int next = 0;
422        for (final int rgb : rgbs) {
423            result[next++] = rgb;
424        }
425        Arrays.sort(result);
426
427        return new SimplePalette(result);
428    }
429
430    public boolean isGrayscale(final BufferedImage src) {
431        final int width = src.getWidth();
432        final int height = src.getHeight();
433
434        if (ColorSpace.TYPE_GRAY == src.getColorModel().getColorSpace().getType()) {
435            return true;
436        }
437
438        for (int y = 0; y < height; y++) {
439            for (int x = 0; x < width; x++) {
440                final int argb = src.getRGB(x, y);
441
442                final int red = 0xff & (argb >> 16);
443                final int green = 0xff & (argb >> 8);
444                final int blue = 0xff & (argb >> 0);
445
446                if (red != green || red != blue) {
447                    return false;
448                }
449            }
450        }
451        return true;
452    }
453
454    public boolean hasTransparency(final BufferedImage src) {
455        return hasTransparency(src, 255);
456    }
457
458    public boolean hasTransparency(final BufferedImage src, final int threshold) {
459        final int width = src.getWidth();
460        final int height = src.getHeight();
461
462        if (!src.getColorModel().hasAlpha()) {
463            return false;
464        }
465
466        for (int y = 0; y < height; y++) {
467            for (int x = 0; x < width; x++) {
468                final int argb = src.getRGB(x, y);
469                final int alpha = 0xff & (argb >> 24);
470                if (alpha < threshold) {
471                    return true;
472                }
473            }
474        }
475        return false;
476    }
477
478    public int countTrasparentColors(final int[] rgbs) {
479        int first = -1;
480
481        for (final int rgb : rgbs) {
482            final int alpha = 0xff & (rgb >> 24);
483            if (alpha < 0xff) {
484                if (first < 0) {
485                    first = rgb;
486                } else if (rgb != first) {
487                    return 2; // more than one transparent color;
488                }
489            }
490        }
491
492        if (first < 0) {
493            return 0;
494        }
495        return 1;
496    }
497
498    public int countTransparentColors(final BufferedImage src) {
499        final ColorModel cm = src.getColorModel();
500        if (!cm.hasAlpha()) {
501            return 0;
502        }
503
504        final int width = src.getWidth();
505        final int height = src.getHeight();
506
507        int first = -1;
508
509        for (int y = 0; y < height; y++) {
510            for (int x = 0; x < width; x++) {
511                final int rgb = src.getRGB(x, y);
512                final int alpha = 0xff & (rgb >> 24);
513                if (alpha < 0xff) {
514                    if (first < 0) {
515                        first = rgb;
516                    } else if (rgb != first) {
517                        return 2; // more than one transparent color;
518                    }
519                }
520            }
521        }
522
523        if (first < 0) {
524            return 0;
525        }
526        return 1;
527    }
528
529}