1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
35
36 public class PaletteFactory {
37
38 private static final class DivisionCandidate {
39
40 private final ColorSpaceSubset dstA;
41 private final ColorSpaceSubset dstB;
42
43 DivisionCandidate(final ColorSpaceSubset dstA, final ColorSpaceSubset dstB) {
44
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;
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;
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;
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
245
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
325
326
327
328
329
330 public Palette makeExactRgbPaletteFancy(final BufferedImage src) {
331
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
381
382
383
384
385
386
387 public SimplePalette makeExactRgbPaletteSimple(final BufferedImage src, final int max) {
388
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
417
418
419
420
421
422
423
424
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
432
433
434
435
436
437
438
439 public Palette makeQuantizedRgbPalette(final BufferedImage src, final int max) {
440 final int precision = 6;
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
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 }