// Near Infinity - An Infinity Engine Browser and Editor // Copyright (C) 2001 - 2005 Jon Olav Hauglid // See LICENSE.txt for license information package org.infinity.resource.graphics; import java.awt.Dimension; import java.awt.Graphics2D; import java.awt.GraphicsConfiguration; import java.awt.GraphicsEnvironment; import java.awt.Image; import java.awt.Transparency; import java.awt.image.BufferedImage; import java.awt.image.ColorModel; import java.awt.image.DataBuffer; import java.awt.image.DataBufferByte; import java.awt.image.IndexColorModel; import java.awt.image.VolatileImage; import java.awt.image.WritableRaster; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.Iterator; import java.util.List; import java.util.PriorityQueue; import javax.imageio.ImageIO; import javax.imageio.ImageReader; import javax.imageio.stream.ImageInputStream; import org.infinity.util.DynamicArray; import org.infinity.util.io.StreamUtils; /** * Contains a set of color-related static methods (little endian order only). */ public class ColorConvert { // max. number of colors for color reduction algorithms private static final int MAX_COLORS = 256; /** * Creates a BufferedImage object in the native color format for best possible performance. * @param width Image width in pixels * @param height Image height in pixels * @param hasTransparency Transparency support * @return A new BufferedImage object with the specified properties. */ public static BufferedImage createCompatibleImage(int width, int height, boolean hasTransparency) { return createCompatibleImage(width, height, hasTransparency ? Transparency.TRANSLUCENT : Transparency.OPAQUE); } /** * Creates a BufferedImage object in the native color format for best possible performance. * @param width Image width in pixels * @param height Image height in pixels * @param transparency The transparency type (either one of {@code Transparency.OPAQUE}, * {@code Transparency.BITMASK} or {@code Transparency.TRANSLUCENT}). * @return A new BufferedImage object with the specified properties. */ public static BufferedImage createCompatibleImage(int width, int height, int transparency) { if (transparency == Transparency.TRANSLUCENT) { return new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); } else { // obtain the current system's graphical settings final GraphicsConfiguration gfxConfig = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration(); return gfxConfig.createCompatibleImage(width, height, transparency); } } /** * Creates a VolatileImage object in the native color format for best possible performance. * @param width Image width in pixels * @param height Image height in pixels * @param hasTransparency Transparency support * @return A new VolatileImage object with the specified properties. */ public static VolatileImage createVolatileImage(int width, int height, boolean hasTransparency) { return createVolatileImage(width, height, hasTransparency ? Transparency.TRANSLUCENT : Transparency.OPAQUE); } /** * Creates a VolatileImage object in the native color format for best possible performance. * @param width Image width in pixels * @param height Image height in pixels * @param transparency The transparency type (either one of {@code Transparency.OPAQUE}, * {@code Transparency.BITMASK} or {@code Transparency.TRANSLUCENT}). * @return A new VolatileImage object with the specified properties. */ public static VolatileImage createVolatileImage(int width, int height, int transparency) { GraphicsConfiguration gc = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration(); return gc.createCompatibleVolatileImage(width, height, transparency); } /** * Converts a generic image object into a BufferedImage object if possible. * @param img The image to convert into a BufferedImage object. * @param hasTransparency Indicates whether the converted image should support transparency. * (Does nothing if the specified image object is already a BufferedImage object.) * @return A BufferedImage object of the specified image. */ public static BufferedImage toBufferedImage(Image img, boolean hasTransparency) { return toBufferedImage(img, hasTransparency, true); } /** * Converts a generic image object into a BufferedImage object if possible. * Returns a BufferedImage object that is guaranteed to be either in true color or indexed color format. * @param img The image to convert into a BufferedImage object. * @param hasTransparency Indicates whether the converted image should support transparency. * (Does nothing if the specified image object is already a BufferedImage object.) * @param forceTrueColor Indicates whether the returned BufferedImage object will alway be in * true color color format (i.e. pixels in ARGB format, with or without transparency support). * @return A BufferedImage object of the specified image in either BufferedImage.TYPE_BYTE_INDEXED format * or one of the true color formats, Returns {@code null} on error. */ public static BufferedImage toBufferedImage(Image img, boolean hasTransparency, boolean forceTrueColor) { if (img != null) { if (img instanceof BufferedImage) { BufferedImage srcImage = (BufferedImage)img; int type = srcImage.getRaster().getDataBuffer().getDataType(); if (type == DataBuffer.TYPE_INT) { return srcImage; } else if (!forceTrueColor) { // attempting to convert special formats into 8-bit paletted image format BufferedImage dstImage = convertToIndexedImage(srcImage); if (dstImage != null) { return dstImage; } } } final BufferedImage image = createCompatibleImage(img.getWidth(null), img.getHeight(null), hasTransparency); Graphics2D g = image.createGraphics(); try { g.drawImage(img, 0, 0, null); } finally { g.dispose(); g = null; } return image; } return null; } /** * Attempts to create a deep copy of the specified BufferedImage object without losing any of its * original properties. Creates a truecolored BufferedImage object if source image format is not * fully supported. * Note: Only indexed and truecolored BufferedImage objects are fully supported! * @param image The image object to clone. * @return A new BufferedImage object possessing the content and properties of the source image. * Returns {@code null} on error. */ public static BufferedImage cloneImage(BufferedImage image) { BufferedImage dstImage = null; if (image != null) { ColorModel cm = image.getColorModel(); boolean isAlphaPreMultiplied = cm.isAlphaPremultiplied(); WritableRaster raster = image.copyData(null); dstImage = new BufferedImage(cm, raster, isAlphaPreMultiplied, null); } return dstImage; } /** * Attempts to retrieve the width and height of the specified image file without loading it completely. * @param fileName The image filename. * @return The image dimensions. */ public static Dimension getImageDimension(Path fileName) { Dimension d = new Dimension(); try (ImageInputStream iis = ImageIO.createImageInputStream(StreamUtils.getInputStream(fileName))) { final Iterator<ImageReader> readers = ImageIO.getImageReaders(iis); if (readers.hasNext()) { ImageReader reader = readers.next(); try { reader.setInput(iis); d.width = reader.getWidth(0); d.height = reader.getHeight(0); } finally { reader.dispose(); } } } catch (Exception e) { d.width = d.height = 0; } return d; } /** * Calculates the nearest color available in the palette for the specified color value. * Note: For better color matching results the palette is expected in HCL color model. * @param rgbColor The source color value in ARGB format (A is ignored). * @param hclPalette A HCL palette with the available color entries. Use the method * {@link #toHclPalette(int[], int[])} to convert a RGB palette into the HCL format. * @return The palette index pointing to the nearest color, or -1 on error. */ public static int nearestColor(int rgbColor, int[] hclPalette) { int index = -1; if (hclPalette != null && hclPalette.length > 0) { int distance = Integer.MAX_VALUE; int v = rgbToHcl(rgbColor); int h = (byte)((v >>> 16) & 0xff), s = (byte)((v >>> 8) & 0xff), l = (byte)(v & 0xff); for (int i = 0; i < hclPalette.length; i++) { int h2 = (byte)((hclPalette[i] >>> 16) & 0xff); int s2 = (byte)((hclPalette[i] >>> 8) & 0xff); int l2 = (byte)(hclPalette[i] & 0xff); int dh = (h2 - h); int ds = (s2 - s); int dl = (l2 - l); int curDistance = dh*dh + ds*ds + dl*dl; if (curDistance < distance) { distance = curDistance; index = i; if (distance == 0) break; } } } return index; } /** * Converts an array of colors from the RGB color model into the HCL color model. This is needed * if you want to use the method {@link #nearestColor(int, int[])}. * @param rgbPalette The source RGB palette. * @param hclPalette An array to store the resulting HCL colors into. * @return {@code true} if the conversion finished successfully, {@code false} otherwise. */ public static boolean toHclPalette(int[] rgbPalette, int[] hclPalette) { if (rgbPalette != null && hclPalette != null && hclPalette.length >= rgbPalette.length) { for (int i = 0; i < rgbPalette.length; i++) { hclPalette[i] = rgbToHcl(rgbPalette[i]); } return true; } return false; } /** * Converts an RGB color value into a normalized HCL color (hue, chroma, luminance). * @param color The color value in ARGB format (A is ignored). * @return The normalized HCL representation of the color. Range of each component: [-128..127] */ public static int rgbToHcl(int color) { // using HCL (hue, chrome, luminance) approach float r = (float)((color >>> 16) & 0xff) / 255.0f; float g = (float)((color >>> 8) & 0xff) / 255.0f; float b = (float)(color & 0xff) / 255.0f; float cmax = r; if (g > cmax) cmax = g; if (b > cmax) cmax = b; float cmin = r; if (g < cmin) cmin = g; if (b < cmin) cmin = b; float cdelta = cmax - cmin; float h, c, l; l = (cmax + cmin) / 2.0f; if (l < 0.0f) l = 0.0f; else if (l > 1.0f) l = 1.0f; if (cdelta == 0.0f) { h = 0.0f; c = 0.0f; } else { c = cdelta; final float cdelta2 = cdelta / 2.0f; float dr = (((cmax - r) / 6.0f) + cdelta2) / cdelta; float dg = (((cmax - g) / 6.0f) + cdelta2) / cdelta; float db = (((cmax - b) / 6.0f) + cdelta2) / cdelta; final float c13 = 1.0f/3.0f; final float c23 = 2.0f/3.0f; if (r == cmax) { h = db - dg; } else if (g == cmax) { h = c13 + dr - db; } else { h = c23 + dg - dr; } if (h < 0.0f) h += 1.0f; if (h > 1.0f) h -= 1.0f; } // normalizing: h = [0..2], c = [0..1], l = [-1..1] h *= 2.0f; l = (l - 0.5f) * 2.0f; double x= c * Math.cos(h*Math.PI); double y = c * Math.sin(h*Math.PI); double z = l; // re-normalizing values for conversion into integer range [-128..127] x = Math.floor(x * 127.5); y = Math.floor(y * 127.5); z = Math.floor(z * 127.5); return (((int)x & 0xff) << 16) | (((int)y & 0xff) << 8) | ((int)z & 0xff); } /** * Reduces the number of colors of the specified pixel data block. * @param pixels The pixel block of the image in ARGB format (alpha is ignored). * @param desiredColors The resulting number of colors after reduction (range 1..256). * @param ignoreAlpha If {@code false}, only visible color values (alpha > 0) will be counted. * @return An array containing the resulting colors, or {@code null} on error. */ public static int[] medianCut(int[] pixels, int desiredColors, boolean ignoreAlpha) { if (desiredColors > 0 && desiredColors <= MAX_COLORS) { int[] pal = new int[desiredColors]; if (medianCut(pixels, desiredColors, pal, ignoreAlpha)) { return pal; } else { pal = null; } } return null; } /** * Reduces the number of colors of the specified pixel data block. * @param pixels The pixel block of the image in ARGB format (alpha is ignored). * @param desiredColors The resulting number of colors after reduction (range 1..256). * @param palette The array to write the resulting colors into. * @param ignoreAlpha If {@code false}, only visible color values (alpha > 0) will be counted. * @return {@code true} if color reduction succeeded, {@code false} otherwise. */ public static boolean medianCut(int[] pixels, int desiredColors, int[] palette, boolean ignoreAlpha) { if (pixels == null || palette == null) throw new NullPointerException(); if (desiredColors > 0 && desiredColors <= MAX_COLORS && palette.length >= desiredColors) { PriorityQueue<PixelBlock> blockQueue = new PriorityQueue<PixelBlock>(desiredColors, PixelBlock.PixelBlockComparator); Pixel[] p = null; if (ignoreAlpha) { p = new Pixel[pixels.length]; for (int i = 0; i < p.length; i++) { p[i] = new Pixel(pixels[i]); } } else { int len = 0; for (int i = 0; i < pixels.length; i++) { if ((pixels[i] & 0xff000000) != 0) len++; } p = new Pixel[len]; for (int i = 0, idx = 0; i < pixels.length && idx < len; i++) { if ((pixels[i] & 0xff000000) != 0) p[idx++] = new Pixel(pixels[i]); } } PixelBlock initialBlock = new PixelBlock(p); initialBlock.shrink(); blockQueue.add(initialBlock); while (blockQueue.size() < desiredColors) { PixelBlock longestBlock = blockQueue.poll(); int ofsBegin = longestBlock.offset(); int ofsMedian = longestBlock.offset() + (longestBlock.size() + 1) / 2; int ofsEnd = longestBlock.offset() + longestBlock.size(); Arrays.sort(longestBlock.getPixels(), longestBlock.offset(), longestBlock.offset() + longestBlock.size(), Pixel.PixelComparator.get(longestBlock.getLongestSideIndex())); PixelBlock block1 = new PixelBlock(longestBlock.getPixels(), ofsBegin, ofsMedian - ofsBegin); PixelBlock block2 = new PixelBlock(longestBlock.getPixels(), ofsMedian, ofsEnd - ofsMedian); block1.shrink(); block2.shrink(); blockQueue.add(block1); blockQueue.add(block2); } int palIndex = 0; while (!blockQueue.isEmpty() && palIndex < desiredColors) { PixelBlock block = blockQueue.poll(); int[] sum = {0, 0, 0}; for (int i = 0; i < block.size(); i++) { for (int j = 0; j < Pixel.MAX_SIZE; j++) { sum[j] += block.getPixel(i).getElement(j); } } Pixel avgPixel = new Pixel(); if (block.size() > 0) { for (int i = 0; i < Pixel.MAX_SIZE; i++) { avgPixel.color[i] = (byte)(sum[i] / block.size()); } } palette[palIndex++] = avgPixel.toColor(); } blockQueue.clear(); return true; } return false; } /** * Attempts to load a palette from the specified Windows BMP file. * @param file The Windows BMP file to extract the palette from. * @return The palette as ARGB integers. * @throws Exception on error. */ public static int[] loadPaletteBMP(Path file) throws Exception { if (file != null && Files.isRegularFile(file)) { try (InputStream is = StreamUtils.getInputStream(file)) { byte[] signature = new byte[8]; is.read(signature); if ("BM".equals(new String(signature, 0, 2))) { // extracting palette from BMP file byte[] header = new byte[54]; System.arraycopy(signature, 0, header, 0, signature.length); is.read(header, signature.length, header.length - signature.length); if (DynamicArray.getInt(header, 0x0e) == 0x28 && // correct BMP header size DynamicArray.getInt(header, 0x12) > 0 && // valid width DynamicArray.getInt(header, 0x16) > 0 && // valid height (DynamicArray.getShort(header, 0x1c) == 4 || // either 4bpp DynamicArray.getInt(header, 0x1c) == 8) && // or 8bpp DynamicArray.getInt(header, 0x1e) == 0) { // no special encoding int bpp = DynamicArray.getUnsignedShort(header, 0x1c); int colorCount = 1 << bpp; byte[] palette = new byte[colorCount*4]; is.read(palette); int[] retVal = new int[colorCount]; for (int i =0; i < colorCount; i++) { retVal[i] = DynamicArray.getInt(palette, i << 2) & 0x00ffffff; } return retVal; } else { throw new Exception("Error loading palette from BMP file " + file.getFileName()); } } else { throw new Exception("Invalid BMP file " + file.getFileName()); } } catch (IOException e) { e.printStackTrace(); throw new Exception("Unable to read BMP file " + file.getFileName()); } } else { throw new Exception("File does not exist."); } } /** * Attempts to load a palette from the specified Windows PAL file. * @param file The Windows PAL file to load. * @return The palette as ARGB integers. * @throws Exception on error. */ public static int[] loadPalettePAL(Path file) throws Exception { if (file != null && Files.isRegularFile(file)) { try (InputStream is = StreamUtils.getInputStream(file)) { byte[] signature = new byte[8]; is.read(signature); if ("RIFF".equals(new String(signature, 0, 4))) { // extracting palette from Windows palette file byte[] signature2 = new byte[8]; is.read(signature2); if ("PAL data".equals(new String(signature2))) { byte[] header = new byte[8]; is.read(header); int numColors = DynamicArray.getUnsignedShort(header, 6); if (numColors >= 2 && numColors <= 256) { byte[] palData = new byte[numColors << 2]; is.read(palData); int[] retVal = new int[numColors]; for (int i = 0; i < numColors; i++) { int col = DynamicArray.getInt(palData, i << 2); retVal[i] = ((col << 16) & 0xff0000) | (col & 0x00ff00) | ((col >> 16) & 0x0000ff); } return retVal; } else { throw new Exception("Invalid number of color entries in Windows palette file " + file.getFileName()); } } else { throw new Exception("Error loading palette from Windows palette file " + file.getFileName()); } } else { throw new Exception("Invalid Windows palette file " + file.getFileName()); } } catch (IOException e) { e.printStackTrace(); throw new Exception("Unable to read Windows palette file " + file.getFileName()); } } else { throw new Exception("File does not exist."); } } /** * Attempts to load a palette from the specified Adobe Color Table file. * @param file The Adobe Color Table file to load. * @return The palette as ARGB integers. * @throws Exception on error. */ public static int[] loadPaletteACT(Path file) throws Exception { if (file != null && Files.isRegularFile(file)) { try (InputStream is = StreamUtils.getInputStream(file)) { int size = (int)Files.size(file); if (size == 768) { byte[] palData = new byte[size]; is.read(palData); int count = size / 3; int[] retVal = new int[count]; for (int ofs = 0, i = 0; i < count; i++, ofs += 3) { retVal[i] = ((palData[ofs] & 0xff) << 16) | ((palData[ofs+1] & 0xff) << 8) | (palData[ofs+2] & 0xff); } return retVal; } else { throw new Exception("Invalid Adobe Photoshop palette file " + file.getFileName()); } } catch (IOException e) { e.printStackTrace(); throw new Exception("Unable to read Adobe Photoshop palette file " + file.getFileName()); } } else { throw new Exception("File does not exist."); } } /** * Attempts to load a palette from the specified BAM file. * @param file The BAM file to extract the palette from. * @return The palette as ARGB integers. * @throws Exception on error. */ public static int[] loadPaletteBAM(Path file) throws Exception { if (file != null && Files.isRegularFile(file)) { try (InputStream is = StreamUtils.getInputStream(file)) { byte[] signature = new byte[8]; is.read(signature); String s = new String(signature); if ("BAM V1 ".equals(s) || "BAMCV1 ".equals(s)) { byte[] bamData = new byte[(int)Files.size(file)]; System.arraycopy(signature, 0, bamData, 0, signature.length); is.read(bamData, signature.length, bamData.length - signature.length); if ("BAMCV1 ".equals(s)) { bamData = Compressor.decompress(bamData); } // extracting palette from BAM v1 file int ofs = DynamicArray.getInt(bamData, 0x10); if (ofs >= 0x18 && ofs < bamData.length - 1024) { int[] retVal = new int[256]; for (int i = 0; i < 256; i++) { retVal[i] = DynamicArray.getInt(bamData, ofs+(i << 2)) & 0x00ffffff; } return retVal; } else { throw new Exception("Error loading palette from BAM file " + file.getFileName()); } } else { throw new Exception("Unsupport file type."); } } catch (IOException e) { e.printStackTrace(); throw new Exception("Unable to read BAM file " + file.getFileName()); } } else { throw new Exception("File does not exist."); } } /** * Attempts to convert a 1-, 2- or 4-bit paletted image or a grayscale image into an 8-bit paletted * image. * @param image The image to convert. * @return The converted image if the source format is compatible with a 256 color table, * {@code null} otherwise. */ private static BufferedImage convertToIndexedImage(BufferedImage image) { if (image != null) { if (image.getType() == BufferedImage.TYPE_BYTE_BINARY) { // converting 1-, 2-, 4-bit image data into paletted image int[] cmap = new int[256]; IndexColorModel srcPal = (IndexColorModel)image.getColorModel(); int bits = srcPal.getPixelSize(); int bitMask = (1 << bits) - 1; int numColors = 1 << bits; for (int i = 0; i < 256; i++) { if (i < numColors) { cmap[i] = srcPal.getRGB(i) | (srcPal.hasAlpha() ? (srcPal.getAlpha(i) << 24) : 0xff000000); } else { cmap[i] = 0xff000000; } } IndexColorModel dstPal; dstPal = new IndexColorModel(8, 256, cmap, 0, srcPal.hasAlpha(), -1, DataBuffer.TYPE_BYTE); BufferedImage dstImage = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_BYTE_INDEXED, dstPal); byte[] src = ((DataBufferByte)image.getRaster().getDataBuffer()).getData(); byte[] dst = ((DataBufferByte)dstImage.getRaster().getDataBuffer()).getData(); int srcOfs = 0, srcBitPos = 8 - bits, dstOfs = 0; while (dstOfs < dst.length) { dst[dstOfs] = (byte)((src[srcOfs] >>> srcBitPos) & bitMask); srcBitPos -= bits; if (srcBitPos < 0) { srcBitPos += 8; srcOfs++; } dstOfs++; } cmap = null; src = null; dst = null; return dstImage; } else if (image.getType() == BufferedImage.TYPE_BYTE_GRAY) { // converting grayscaled image with implicit palette into indexed image with explicit palette int[] cmap = new int[256]; for (int i = 0; i < cmap.length; i++) { cmap[i] = (i << 16) | (i << 8) | i; } IndexColorModel cm = new IndexColorModel(8, 256, cmap, 0, false, -1, DataBuffer.TYPE_BYTE); BufferedImage dstImage = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_BYTE_INDEXED, cm); byte[] src = ((DataBufferByte)image.getRaster().getDataBuffer()).getData(); byte[] dst = ((DataBufferByte)dstImage.getRaster().getDataBuffer()).getData(); System.arraycopy(src, 0, dst, 0, src.length); cmap = null; src = null; dst = null; return dstImage; } else if (image.getType() == BufferedImage.TYPE_BYTE_INDEXED) { return image; } } return null; } //-------------------------- INNER CLASSES -------------------------- private static class PixelBlock { private final Pixel minCorner, maxCorner; private final Pixel[] pixels; private final int ofs, len; public PixelBlock(Pixel[] pixels) { this(pixels, 0, pixels != null ? pixels.length : 0); } public PixelBlock(Pixel[] pixels, int ofs, int len) { if (pixels == null) throw new NullPointerException(); this.pixels = pixels; this.ofs = ofs; this.len = len; minCorner = new Pixel(Byte.MIN_VALUE, Byte.MIN_VALUE, Byte.MIN_VALUE); maxCorner = new Pixel(Byte.MAX_VALUE, Byte.MAX_VALUE, Byte.MAX_VALUE); } public Pixel[] getPixels() { return pixels; } public int size() { return len; } public int offset() { return ofs; } public Pixel getPixel(int index) { if (index >= 0 && index < len) { return pixels[ofs + index]; } else { return new Pixel(0); } } public int getLongestSideIndex() { int m = Integer.MIN_VALUE, maxIndex = -1; for (int i = 0; i < Pixel.MAX_SIZE; i++) { int diff = maxCorner.getElement(i) - minCorner.getElement(i); if (diff > m) { m = diff; maxIndex = i; } } return maxIndex; } public int getLongestSideLength() { int i = getLongestSideIndex(); return maxCorner.getElement(i) - minCorner.getElement(i); } public void shrink() { if (len > 0) { for (int i = 0; i < Pixel.MAX_SIZE; i++) { minCorner.color[i] = maxCorner.color[i] = pixels[ofs].color[i]; } } else { for (int i = 0; i < Pixel.MAX_SIZE; i++) { minCorner.color[i] = maxCorner.color[i] = 0; } } for (int i = ofs; i < ofs + len; i++) { for (int j = 0; j < Pixel.MAX_SIZE; j++) { if (pixels[i].getElement(j) < minCorner.getElement(j)) minCorner.color[j] = pixels[i].color[j]; if (pixels[i].getElement(j) > maxCorner.getElement(j)) maxCorner.color[j] = pixels[i].color[j]; } } } public static Comparator<PixelBlock> PixelBlockComparator = new Comparator<PixelBlock>() { @Override public int compare(PixelBlock pb1, PixelBlock pb2) { // inverting natural order by switching sides return pb2.getLongestSideLength() - pb1.getLongestSideLength(); } }; } private static class Pixel { public static final int MAX_SIZE = 3; public final byte[] color; public Pixel() { this.color = new byte[]{0, 0, 0}; } public Pixel(int color) { this.color = new byte[]{(byte)((color >>> 16) & 0xff), (byte)((color >>> 8) & 0xff), (byte)(color & 0xff)}; } public Pixel(byte r, byte g, byte b) { this.color = new byte[]{r, g, b}; } public int toColor() { return ((color[0] & 0xff) << 16) | ((color[1] & 0xff) << 8) | (color[2] & 0xff); } public int getElement(int index) { if (index >= 0 && index < MAX_SIZE) { return (color[index] & 0xff); } else { return 0; } } public static List<Comparator<Pixel>> PixelComparator = new ArrayList<Comparator<Pixel>>(3); static { PixelComparator.add(new Comparator<Pixel>() { @Override public int compare(Pixel p1, Pixel p2) { return p1.getElement(0) - p2.getElement(0); } }); PixelComparator.add(new Comparator<Pixel>() { @Override public int compare(Pixel p1, Pixel p2) { return p1.getElement(1) - p2.getElement(1); } }); PixelComparator.add(new Comparator<Pixel>() { @Override public int compare(Pixel p1, Pixel p2) { return p1.getElement(2) - p2.getElement(2); } }); } } }