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.ImagingException; 031import org.apache.commons.imaging.common.Allocator; 032 033/** 034 * Factory for creating palettes. 035 */ 036public class PaletteFactory { 037 038 private static final class DivisionCandidate { 039 // private final ColorSpaceSubset src; 040 private final ColorSpaceSubset dstA; 041 private final ColorSpaceSubset dstB; 042 043 DivisionCandidate(final ColorSpaceSubset dstA, final ColorSpaceSubset dstB) { 044 // this.src = src; 045 this.dstA = dstA; 046 this.dstB = dstB; 047 } 048 } 049 050 private static final Logger LOGGER = Logger.getLogger(PaletteFactory.class.getName()); 051 052 public static final int COMPONENTS = 3; // in bits 053 054 public int countTransparentColors(final BufferedImage src) { 055 final ColorModel cm = src.getColorModel(); 056 if (!cm.hasAlpha()) { 057 return 0; 058 } 059 060 final int width = src.getWidth(); 061 final int height = src.getHeight(); 062 063 int first = -1; 064 065 for (int y = 0; y < height; y++) { 066 for (int x = 0; x < width; x++) { 067 final int rgb = src.getRGB(x, y); 068 final int alpha = 0xff & rgb >> 24; 069 if (alpha < 0xff) { 070 if (first < 0) { 071 first = rgb; 072 } else if (rgb != first) { 073 return 2; // more than one transparent color; 074 } 075 } 076 } 077 } 078 079 if (first < 0) { 080 return 0; 081 } 082 return 1; 083 } 084 085 public int countTrasparentColors(final int[] rgbs) { 086 int first = -1; 087 088 for (final int rgb : rgbs) { 089 final int alpha = 0xff & rgb >> 24; 090 if (alpha < 0xff) { 091 if (first < 0) { 092 first = rgb; 093 } else if (rgb != first) { 094 return 2; // more than one transparent color; 095 } 096 } 097 } 098 099 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}