View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.imaging.palette;
18  
19  import java.awt.color.ColorSpace;
20  import java.awt.image.BufferedImage;
21  import java.awt.image.ColorModel;
22  import java.util.ArrayList;
23  import java.util.Arrays;
24  import java.util.HashSet;
25  import java.util.List;
26  import java.util.Set;
27  import java.util.logging.Level;
28  import java.util.logging.Logger;
29  
30  import org.apache.commons.imaging.ImagingException;
31  import org.apache.commons.imaging.common.Allocator;
32  
33  /**
34   * Factory for creating palettes.
35   */
36  public class PaletteFactory {
37  
38      private static final class DivisionCandidate {
39          // private final ColorSpaceSubset src;
40          private final ColorSpaceSubset dstA;
41          private final ColorSpaceSubset dstB;
42  
43          DivisionCandidate(final ColorSpaceSubset dstA, final ColorSpaceSubset dstB) {
44              // this.src = src;
45              this.dstA = dstA;
46              this.dstB = dstB;
47          }
48      }
49  
50      private static final Logger LOGGER = Logger.getLogger(PaletteFactory.class.getName());
51  
52      public static final int COMPONENTS = 3; // in bits
53  
54      public int countTransparentColors(final BufferedImage src) {
55          final ColorModel cm = src.getColorModel();
56          if (!cm.hasAlpha()) {
57              return 0;
58          }
59  
60          final int width = src.getWidth();
61          final int height = src.getHeight();
62  
63          int first = -1;
64  
65          for (int y = 0; y < height; y++) {
66              for (int x = 0; x < width; x++) {
67                  final int rgb = src.getRGB(x, y);
68                  final int alpha = 0xff & rgb >> 24;
69                  if (alpha < 0xff) {
70                      if (first < 0) {
71                          first = rgb;
72                      } else if (rgb != first) {
73                          return 2; // more than one transparent color;
74                      }
75                  }
76              }
77          }
78  
79          if (first < 0) {
80              return 0;
81          }
82          return 1;
83      }
84  
85      public int countTrasparentColors(final int[] rgbs) {
86          int first = -1;
87  
88          for (final int rgb : rgbs) {
89              final int alpha = 0xff & rgb >> 24;
90              if (alpha < 0xff) {
91                  if (first < 0) {
92                      first = rgb;
93                  } else if (rgb != first) {
94                      return 2; // more than one transparent color;
95                  }
96              }
97          }
98  
99          if (first < 0) {
100             return 0;
101         }
102         return 1;
103     }
104 
105     private void divide(final List<ColorSpaceSubset> v, final int desiredCount, final int[] table, final int precision) {
106         final List<ColorSpaceSubset> ignore = new ArrayList<>();
107 
108         while (true) {
109             int maxArea = -1;
110             ColorSpaceSubset maxSubset = null;
111 
112             for (final ColorSpaceSubset subset : v) {
113                 if (ignore.contains(subset)) {
114                     continue;
115                 }
116                 final int area = subset.total;
117 
118                 if (maxSubset == null || area > maxArea) {
119                     maxSubset = subset;
120                     maxArea = area;
121                 }
122             }
123 
124             if (maxSubset == null) {
125                 return;
126             }
127             if (LOGGER.isLoggable(Level.FINEST)) {
128                 LOGGER.finest("\t" + "area: " + maxArea);
129             }
130 
131             final DivisionCandidate dc = divideSubset2(table, maxSubset, precision);
132             if (dc != null) {
133                 v.remove(maxSubset);
134                 v.add(dc.dstA);
135                 v.add(dc.dstB);
136             } else {
137                 ignore.add(maxSubset);
138             }
139 
140             if (v.size() == desiredCount) {
141                 return;
142             }
143         }
144     }
145 
146     private DivisionCandidate divideSubset2(final int[] table, final ColorSpaceSubset subset, final int precision) {
147         final List<DivisionCandidate> dcs = new ArrayList<>(divideSubset2(table, subset, 0, precision));
148 
149         dcs.addAll(divideSubset2(table, subset, 1, precision));
150         dcs.addAll(divideSubset2(table, subset, 2, precision));
151 
152         DivisionCandidate bestV = null;
153         double bestScore = Double.MAX_VALUE;
154 
155         for (final DivisionCandidate dc : dcs) {
156             final ColorSpaceSubset first = dc.dstA;
157             final ColorSpaceSubset second = dc.dstB;
158             final int area1 = first.total;
159             final int area2 = second.total;
160 
161             final int diff = Math.abs(area1 - area2);
162             final double score = (double) diff / (double) Math.max(area1, area2);
163 
164             if (bestV == null || score < bestScore) {
165                 bestV = dc;
166                 bestScore = score;
167             }
168 
169         }
170 
171         return bestV;
172     }
173 
174     private List<DivisionCandidate> divideSubset2(final int[] table, 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 = Arrays.copyOf(subset.mins, subset.mins.length);
182         final int[] sliceMaxs = Arrays.copyOf(subset.maxs, subset.maxs.length);
183 
184         int sum1 = 0;
185         int slice1;
186         int last = 0;
187 
188         for (slice1 = subset.mins[component]; slice1 != subset.maxs[component] + 1; slice1++) {
189             sliceMins[component] = slice1;
190             sliceMaxs[component] = slice1;
191 
192             last = getFrequencyTotal(table, sliceMins, sliceMaxs, precision);
193 
194             sum1 += last;
195 
196             if (sum1 >= total / 2) {
197                 break;
198             }
199         }
200 
201         final int sum2 = sum1 - last;
202         final int slice2 = slice1 - 1;
203 
204         final DivisionCandidate dc1 = finishDivision(subset, component, precision, sum1, slice1);
205         final DivisionCandidate dc2 = finishDivision(subset, component, precision, sum2, slice2);
206 
207         final List<DivisionCandidate> result = new ArrayList<>();
208 
209         if (dc1 != null) {
210             result.add(dc1);
211         }
212         if (dc2 != null) {
213             result.add(dc2);
214         }
215 
216         return result;
217     }
218 
219     private DivisionCandidate finishDivision(final ColorSpaceSubset subset, final int component, final int precision, final int sum, final int slice) {
220         if (LOGGER.isLoggable(Level.FINEST)) {
221             subset.dump("trying (" + component + "): ");
222         }
223 
224         final int total = subset.total;
225 
226         if (slice < subset.mins[component] || slice >= subset.maxs[component]) {
227             return null;
228         }
229 
230         if (sum < 1 || sum >= total) {
231             return null;
232         }
233 
234         final int[] sliceMins = Arrays.copyOf(subset.mins, subset.mins.length);
235         final int[] sliceMaxs = Arrays.copyOf(subset.maxs, subset.maxs.length);
236 
237         sliceMaxs[component] = slice;
238         sliceMins[component] = slice + 1;
239 
240         if (LOGGER.isLoggable(Level.FINEST)) {
241             LOGGER.finest("total: " + total);
242             LOGGER.finest("first total: " + sum);
243             LOGGER.finest("second total: " + (total - sum));
244             // System.out.println("start: " + start);
245             // System.out.println("end: " + end);
246             LOGGER.finest("slice: " + slice);
247 
248         }
249 
250         final ColorSpaceSubset first = new ColorSpaceSubset(sum, precision, subset.mins, sliceMaxs);
251         final ColorSpaceSubset second = new ColorSpaceSubset(total - sum, precision, sliceMins, subset.maxs);
252 
253         return new DivisionCandidate(first, second);
254 
255     }
256 
257     private int getFrequencyTotal(final int[] table, final int[] mins, final int[] maxs, final int precision) {
258         int sum = 0;
259 
260         for (int blue = mins[2]; blue <= maxs[2]; blue++) {
261             final int b = blue << 2 * precision;
262             for (int green = mins[1]; green <= maxs[1]; green++) {
263                 final int g = green << 1 * precision;
264                 for (int red = mins[0]; red <= maxs[0]; red++) {
265                     final int index = b | g | red;
266 
267                     sum += table[index];
268                 }
269             }
270         }
271 
272         return sum;
273     }
274 
275     public boolean hasTransparency(final BufferedImage src) {
276         return hasTransparency(src, 255);
277     }
278 
279     public boolean hasTransparency(final BufferedImage src, final int threshold) {
280         final int width = src.getWidth();
281         final int height = src.getHeight();
282 
283         if (!src.getColorModel().hasAlpha()) {
284             return false;
285         }
286 
287         for (int y = 0; y < height; y++) {
288             for (int x = 0; x < width; x++) {
289                 final int argb = src.getRGB(x, y);
290                 final int alpha = 0xff & argb >> 24;
291                 if (alpha < threshold) {
292                     return true;
293                 }
294             }
295         }
296         return false;
297     }
298 
299     public boolean isGrayscale(final BufferedImage src) {
300         final int width = src.getWidth();
301         final int height = src.getHeight();
302 
303         if (ColorSpace.TYPE_GRAY == src.getColorModel().getColorSpace().getType()) {
304             return true;
305         }
306 
307         for (int y = 0; y < height; y++) {
308             for (int x = 0; x < width; x++) {
309                 final int argb = src.getRGB(x, y);
310 
311                 final int red = 0xff & argb >> 16;
312                 final int green = 0xff & argb >> 8;
313                 final int blue = 0xff & argb >> 0;
314 
315                 if (red != green || red != blue) {
316                     return false;
317                 }
318             }
319         }
320         return true;
321     }
322 
323     /**
324      * Builds an exact complete opaque palette containing all the colors in {@code src}, using an algorithm that is faster than
325      * {@linkplain #makeExactRgbPaletteSimple} for large images but uses 2 mebibytes of working memory. Treats all the colors as opaque.
326      *
327      * @param src the image whose palette to build
328      * @return the palette
329      */
330     public Palette makeExactRgbPaletteFancy(final BufferedImage src) {
331         // map what rgb values have been used
332 
333         final byte[] rgbmap = Allocator.byteArray(256 * 256 * 32);
334 
335         final int width = src.getWidth();
336         final int height = src.getHeight();
337 
338         for (int y = 0; y < height; y++) {
339             for (int x = 0; x < width; x++) {
340                 final int argb = src.getRGB(x, y);
341                 final int rggbb = 0x1fffff & argb;
342                 final int highred = 0x7 & argb >> 21;
343                 final int mask = 1 << highred;
344                 rgbmap[rggbb] |= mask;
345             }
346         }
347 
348         int count = 0;
349         for (final byte element : rgbmap) {
350             final int eight = 0xff & element;
351             count += Integer.bitCount(eight);
352         }
353 
354         if (LOGGER.isLoggable(Level.FINEST)) {
355             LOGGER.finest("Used colors: " + count);
356         }
357 
358         final int[] colormap = Allocator.intArray(count);
359         int mapsize = 0;
360         for (int i = 0; i < rgbmap.length; i++) {
361             final int eight = 0xff & rgbmap[i];
362             int mask = 0x80;
363             for (int j = 0; j < 8; j++) {
364                 final int bit = eight & mask;
365                 mask >>>= 1;
366 
367                 if (bit > 0) {
368                     final int rgb = i | 7 - j << 21;
369 
370                     colormap[mapsize++] = rgb;
371                 }
372             }
373         }
374 
375         Arrays.sort(colormap);
376         return new SimplePalette(colormap);
377     }
378 
379     /**
380      * Builds an exact complete opaque palette containing all the colors in {@code src}, and fails by returning {@code null} if there are more than {@code max}
381      * colors necessary to do this.
382      *
383      * @param src the image whose palette to build
384      * @param max the maximum number of colors the palette can contain
385      * @return the complete palette of {@code max} or less colors, or {@code null} if more than {@code max} colors are necessary
386      */
387     public SimplePalette makeExactRgbPaletteSimple(final BufferedImage src, final int max) {
388         // This is not efficient for large values of max, say, max > 256;
389         final Set<Integer> rgbs = new HashSet<>();
390 
391         final int width = src.getWidth();
392         final int height = src.getHeight();
393 
394         for (int y = 0; y < height; y++) {
395             for (int x = 0; x < width; x++) {
396                 final int argb = src.getRGB(x, y);
397                 final int rgb = 0xffffff & argb;
398 
399                 if (rgbs.add(rgb) && rgbs.size() > max) {
400                     return null;
401                 }
402             }
403         }
404 
405         final int[] result = Allocator.intArray(rgbs.size());
406         int next = 0;
407         for (final int rgb : rgbs) {
408             result[next++] = rgb;
409         }
410         Arrays.sort(result);
411 
412         return new SimplePalette(result);
413     }
414 
415     /**
416      * Builds an inexact possibly translucent palette of at most {@code max} colors in {@code src} using the traditional Median Cut algorithm. Color bounding
417      * boxes are split along the longest axis, with each step splitting the box. All bits in each component are used. The Algorithm is slower and seems exact
418      * than {@linkplain #makeQuantizedRgbPalette(BufferedImage, int)}.
419      *
420      * @param src         the image whose palette to build
421      * @param transparent whether to consider the alpha values
422      * @param max         the maximum number of colors the palette can contain
423      * @return the palette of at most {@code max} colors
424      * @throws ImagingException if it fails to process the palette
425      */
426     public Palette makeQuantizedRgbaPalette(final BufferedImage src, final boolean transparent, final int max) throws ImagingException {
427         return new MedianCutQuantizer(!transparent).process(src, max, new LongestAxisMedianCut());
428     }
429 
430     /**
431      * Builds an inexact opaque palette of at most {@code max} colors in {@code src} using a variation of the Median Cut algorithm. Accurate to 6 bits per
432      * component, and works by splitting the color bounding box most heavily populated by colors along the component which splits the colors in that box most
433      * evenly.
434      *
435      * @param src the image whose palette to build
436      * @param max the maximum number of colors the palette can contain
437      * @return the palette of at most {@code max} colors
438      */
439     public Palette makeQuantizedRgbPalette(final BufferedImage src, final int max) {
440         final int precision = 6; // in bits
441 
442         final int tableScale = precision * COMPONENTS;
443         final int tableSize = 1 << tableScale;
444         final int[] table = Allocator.intArray(tableSize);
445 
446         final int width = src.getWidth();
447         final int height = src.getHeight();
448 
449         final List<ColorSpaceSubset> subsets = new ArrayList<>();
450         final ColorSpaceSubset all = new ColorSpaceSubset(width * height, precision);
451         subsets.add(all);
452 
453         if (LOGGER.isLoggable(Level.FINEST)) {
454             final int preTotal = getFrequencyTotal(table, all.mins, all.maxs, precision);
455             LOGGER.finest("pre total: " + preTotal);
456         }
457 
458         // step 1: count frequency of colors
459         for (int y = 0; y < height; y++) {
460             for (int x = 0; x < width; x++) {
461                 final int argb = src.getRGB(x, y);
462 
463                 final int index = pixelToQuantizationTableIndex(argb, precision);
464 
465                 table[index]++;
466             }
467         }
468 
469         if (LOGGER.isLoggable(Level.FINEST)) {
470             final int allTotal = getFrequencyTotal(table, all.mins, all.maxs, precision);
471             LOGGER.finest("all total: " + allTotal);
472             LOGGER.finest("width * height: " + width * height);
473         }
474 
475         divide(subsets, max, table, precision);
476 
477         if (LOGGER.isLoggable(Level.FINEST)) {
478             LOGGER.finest("subsets: " + subsets.size());
479             LOGGER.finest("width*height: " + width * height);
480         }
481 
482         for (int i = 0; i < subsets.size(); i++) {
483             final ColorSpaceSubset subset = subsets.get(i);
484 
485             subset.setAverageRgb(table);
486 
487             if (LOGGER.isLoggable(Level.FINEST)) {
488                 subset.dump(i + ": ");
489             }
490         }
491 
492         subsets.sort(ColorSpaceSubset.RGB_COMPARATOR);
493 
494         return new QuantizedPalette(subsets, precision);
495     }
496 
497     private int pixelToQuantizationTableIndex(int argb, final int precision) {
498         int result = 0;
499         final int precisionMask = (1 << precision) - 1;
500 
501         for (int i = 0; i < COMPONENTS; i++) {
502             int sample = argb & 0xff;
503             argb >>= 8;
504 
505             sample >>= 8 - precision;
506             result = result << precision | sample & precisionMask;
507         }
508 
509         return result;
510     }
511 
512 }