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