/* * Copyright (c) 2008, Harald Kuhr * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * * Neither the name of the copyright holder nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package com.twelvemonkeys.image; import java.awt.*; import java.awt.geom.AffineTransform; import java.awt.geom.Rectangle2D; import java.awt.image.*; import java.util.Hashtable; /** * This class contains methods for basic image manipulation and conversion. * * @author Harald Kuhr * @author last modified by $Author: haku $ * @version $Id: common/common-image/src/main/java/com/twelvemonkeys/image/ImageUtil.java#3 $ */ public final class ImageUtil { // TODO: Split palette generation out, into ColorModel classes (?) public final static int ROTATE_90_CCW = -90; public final static int ROTATE_90_CW = 90; public final static int ROTATE_180 = 180; public final static int FLIP_VERTICAL = -1; public final static int FLIP_HORIZONTAL = 1; /** * Alias for {@link ConvolveOp#EDGE_ZERO_FILL}. * @see #convolve(java.awt.image.BufferedImage, java.awt.image.Kernel, int) * @see #EDGE_REFLECT */ public static final int EDGE_ZERO_FILL = ConvolveOp.EDGE_ZERO_FILL; /** * Alias for {@link ConvolveOp#EDGE_NO_OP}. * @see #convolve(java.awt.image.BufferedImage, java.awt.image.Kernel, int) * @see #EDGE_REFLECT */ public static final int EDGE_NO_OP = ConvolveOp.EDGE_NO_OP; /** * Adds a border to the image while convolving. The border will reflect the * edges of the original image. This is usually a good default. * Note that while this mode typically provides better quality than the * standard modes {@code EDGE_ZERO_FILL} and {@code EDGE_NO_OP}, it does so * at the expense of higher memory consumption and considerable more computation. * @see #convolve(java.awt.image.BufferedImage, java.awt.image.Kernel, int) */ public static final int EDGE_REFLECT = 2; // as JAI BORDER_REFLECT /** * Adds a border to the image while convolving. The border will wrap the * edges of the original image. This is usually the best choice for tiles. * Note that while this mode typically provides better quality than the * standard modes {@code EDGE_ZERO_FILL} and {@code EDGE_NO_OP}, it does so * at the expense of higher memory consumption and considerable more computation. * @see #convolve(java.awt.image.BufferedImage, java.awt.image.Kernel, int) * @see #EDGE_REFLECT */ public static final int EDGE_WRAP = 3; // as JAI BORDER_WRAP /** * Java default dither */ public final static int DITHER_DEFAULT = IndexImage.DITHER_DEFAULT; /** * No dither */ public final static int DITHER_NONE = IndexImage.DITHER_NONE; /** * Error diffusion dither */ public final static int DITHER_DIFFUSION = IndexImage.DITHER_DIFFUSION; /** * Error diffusion dither with alternating scans */ public final static int DITHER_DIFFUSION_ALTSCANS = IndexImage.DITHER_DIFFUSION_ALTSCANS; /** * Default color selection */ public final static int COLOR_SELECTION_DEFAULT = IndexImage.COLOR_SELECTION_DEFAULT; /** * Prioritize speed */ public final static int COLOR_SELECTION_FAST = IndexImage.COLOR_SELECTION_FAST; /** * Prioritize quality */ public final static int COLOR_SELECTION_QUALITY = IndexImage.COLOR_SELECTION_QUALITY; /** * Default transparency (none) */ public final static int TRANSPARENCY_DEFAULT = IndexImage.TRANSPARENCY_DEFAULT; /** * Discard any alpha information */ public final static int TRANSPARENCY_OPAQUE = IndexImage.TRANSPARENCY_OPAQUE; /** * Convert alpha to bitmask */ public final static int TRANSPARENCY_BITMASK = IndexImage.TRANSPARENCY_BITMASK; /** * Keep original alpha (not supported yet) */ protected final static int TRANSPARENCY_TRANSLUCENT = IndexImage.TRANSPARENCY_TRANSLUCENT; /** Passed to the createXxx methods, to indicate that the type does not matter */ private final static int BI_TYPE_ANY = -1; /* public final static int BI_TYPE_ANY_TRANSLUCENT = -1; public final static int BI_TYPE_ANY_BITMASK = -2; public final static int BI_TYPE_ANY_OPAQUE = -3;*/ /** Tells wether this WM may support acceleration of some images */ private static boolean VM_SUPPORTS_ACCELERATION = true; /** The sharpen matrix */ private static final float[] SHARPEN_MATRIX = new float[] { 0.0f, -0.3f, 0.0f, -0.3f, 2.2f, -0.3f, 0.0f, -0.3f, 0.0f }; /** * The sharpen kernel. Uses the following 3 by 3 matrix: *
| 0.0 | -0.3 | 0.0 |
| -0.3 | 2.2 | -0.3 |
| 0.0 | -0.3 | 0.0 |
* If the image is already a {@code BufferedImage}, it is simply returned * and no conversion takes place. *
* * @param pOriginal the image to convert. * * @return a {@code BufferedImage} */ public static BufferedImage toBuffered(RenderedImage pOriginal) { // Don't convert if it already is a BufferedImage if (pOriginal instanceof BufferedImage) { return (BufferedImage) pOriginal; } if (pOriginal == null) { throw new IllegalArgumentException("original == null"); } // Copy properties Hashtable* If the image is already a {@code BufferedImage} of the given type, it * is simply returned and no conversion takes place. *
* * @param pOriginal the image to convert. * @param pType the type of buffered image * * @return a {@code BufferedImage} * * @throws IllegalArgumentException if {@code pOriginal == null} * or {@code pType} is not a valid type for {@code BufferedImage} * * @see java.awt.image.BufferedImage#getType() */ public static BufferedImage toBuffered(RenderedImage pOriginal, int pType) { // Don't convert if it already is BufferedImage and correct type if ((pOriginal instanceof BufferedImage) && ((BufferedImage) pOriginal).getType() == pType) { return (BufferedImage) pOriginal; } if (pOriginal == null) { throw new IllegalArgumentException("original == null"); } // Create a buffered image BufferedImage image = createBuffered(pOriginal.getWidth(), pOriginal.getHeight(), pType, Transparency.TRANSLUCENT); // Draw the image onto the buffer // NOTE: This is faster than doing a raster conversion in most cases Graphics2D g = image.createGraphics(); try { g.setComposite(AlphaComposite.Src); g.drawRenderedImage(pOriginal, IDENTITY_TRANSFORM); } finally { g.dispose(); } return image; } /** * Converts the {@code BufferedImage} to a {@code BufferedImage} of the * given type. The new image will have the same {@code ColorModel}, * {@code Raster} and properties as the original image, if possible. ** If the image is already a {@code BufferedImage} of the given type, it * is simply returned and no conversion takes place. *
** This method simply invokes * {@link #toBuffered(RenderedImage,int) toBuffered((RenderedImage) pOriginal, pType)}. *
* * @param pOriginal the image to convert. * @param pType the type of buffered image * * @return a {@code BufferedImage} * * @throws IllegalArgumentException if {@code pOriginal == null} * or if {@code pType} is not a valid type for {@code BufferedImage} * * @see java.awt.image.BufferedImage#getType() */ public static BufferedImage toBuffered(BufferedImage pOriginal, int pType) { return toBuffered((RenderedImage) pOriginal, pType); } /** * Converts the {@code Image} to a {@code BufferedImage}. * The new image will have the same {@code ColorModel}, {@code Raster} and * properties as the original image, if possible. ** If the image is already a {@code BufferedImage}, it is simply returned * and no conversion takes place. *
* * @param pOriginal the image to convert. * * @return a {@code BufferedImage} * * @throws IllegalArgumentException if {@code pOriginal == null} * @throws ImageConversionException if the image cannot be converted */ public static BufferedImage toBuffered(Image pOriginal) { // Don't convert if it already is BufferedImage if (pOriginal instanceof BufferedImage) { return (BufferedImage) pOriginal; } if (pOriginal == null) { throw new IllegalArgumentException("original == null"); } //System.out.println("--> Doing full BufferedImage conversion..."); BufferedImageFactory factory = new BufferedImageFactory(pOriginal); return factory.getBufferedImage(); } /** * Creates a deep copy of the given image. The image will have the same * color model and raster type, but will not share image (pixel) data * with the input image. * * @param pImage the image to clone. * * @return a new {@code BufferedImage} * * @throws IllegalArgumentException if {@code pImage} is {@code null} */ public static BufferedImage createCopy(final BufferedImage pImage) { if (pImage == null) { throw new IllegalArgumentException("image == null"); } ColorModel cm = pImage.getColorModel(); BufferedImage img = new BufferedImage(cm, cm.createCompatibleWritableRaster(pImage.getWidth(), pImage.getHeight()), cm.isAlphaPremultiplied(), null); drawOnto(img, pImage); return img; } /** * Creates a {@code WritableRaster} for the given {@code ColorModel} and * pixel data. ** This method is optimized for the most common cases of {@code ColorModel} * and pixel data combinations. The raster's backing {@code DataBuffer} is * created directly from the pixel data, as this is faster and more * resource-friendly than using * {@code ColorModel.createCompatibleWritableRaster(w, h)}. *
** For uncommon combinations, the method will fallback to using * {@code ColorModel.createCompatibleWritableRaster(w, h)} and * {@code WritableRaster.setDataElements(w, h, pixels)} *
** Note that the {@code ColorModel} and pixel data are not cloned * (in most cases). *
* * @param pWidth the requested raster width * @param pHeight the requested raster height * @param pPixels the pixels, as an array, of a type supported by the * different {@link DataBuffer} * @param pColorModel the color model to use * @return a new {@code WritableRaster} * * @throws NullPointerException if either {@code pColorModel} or * {@code pPixels} are {@code null}. * @throws RuntimeException if {@code pWidth} and {@code pHeight} does not * match the pixel data in {@code pPixels}. * * @see ColorModel#createCompatibleWritableRaster(int, int) * @see ColorModel#createCompatibleSampleModel(int, int) * @see WritableRaster#setDataElements(int, int, Object) * @see DataBuffer */ static WritableRaster createRaster(int pWidth, int pHeight, Object pPixels, ColorModel pColorModel) { // NOTE: This is optimized code for most common cases. // We create a DataBuffer from the pixel array directly, // and creating a raster based on the DataBuffer and ColorModel. // Creating rasters this way is faster and more resource-friendly, as // cm.createCompatibleWritableRaster allocates an // "empty" DataBuffer with a storage array of w*h. This array is // later discarded, and replaced in the raster.setDataElements() call. // The "old" way is kept as a more compatible fall-back mode. DataBuffer buffer = null; WritableRaster raster = null; int bands; if (pPixels instanceof int[]) { int[] data = (int[]) pPixels; buffer = new DataBufferInt(data, data.length); bands = pColorModel.getNumComponents(); } else if (pPixels instanceof short[]) { short[] data = (short[]) pPixels; buffer = new DataBufferUShort(data, data.length); bands = data.length / (pWidth * pHeight); } else if (pPixels instanceof byte[]) { byte[] data = (byte[]) pPixels; buffer = new DataBufferByte(data, data.length); // NOTE: This only holds for gray and indexed with one byte per pixel... if (pColorModel instanceof IndexColorModel) { bands = 1; } else { bands = data.length / (pWidth * pHeight); } } else { // Fallback mode, slower & requires more memory, but compatible bands = -1; // Create raster from color model, w and h raster = pColorModel.createCompatibleWritableRaster(pWidth, pHeight); raster.setDataElements(0, 0, pWidth, pHeight, pPixels); // Note: This is known to throw ClassCastExceptions.. } if (raster == null) { if (pColorModel instanceof IndexColorModel && isIndexedPacked((IndexColorModel) pColorModel)) { raster = Raster.createPackedRaster(buffer, pWidth, pHeight, pColorModel.getPixelSize(), LOCATION_UPPER_LEFT); } else if (pColorModel instanceof PackedColorModel) { PackedColorModel pcm = (PackedColorModel) pColorModel; raster = Raster.createPackedRaster(buffer, pWidth, pHeight, pWidth, pcm.getMasks(), LOCATION_UPPER_LEFT); } else { // (A)BGR order... For TYPE_3BYTE_BGR/TYPE_4BYTE_ABGR/TYPE_4BYTE_ABGR_PRE. int[] bandsOffsets = new int[bands]; for (int i = 0; i < bands;) { bandsOffsets[i] = bands - (++i); } raster = Raster.createInterleavedRaster(buffer, pWidth, pHeight, pWidth * bands, bands, bandsOffsets, LOCATION_UPPER_LEFT); } } return raster; } private static boolean isIndexedPacked(IndexColorModel pColorModel) { return (pColorModel.getPixelSize() == 1 || pColorModel.getPixelSize() == 2 || pColorModel.getPixelSize() == 4); } /** * Workaround for bug: TYPE_3BYTE_BGR, TYPE_4BYTE_ABGR and * TYPE_4BYTE_ABGR_PRE are all converted to TYPE_CUSTOM when using the * default createCompatibleWritableRaster from ComponentColorModel. * * @param pOriginal the orignal image * @param pModel the original color model * @param width the requested width of the raster * @param height the requested height of the raster * * @return a new WritableRaster */ static WritableRaster createCompatibleWritableRaster(BufferedImage pOriginal, ColorModel pModel, int width, int height) { if (pModel == null || equals(pOriginal.getColorModel(), pModel)) { int[] bOffs; switch (pOriginal.getType()) { case BufferedImage.TYPE_3BYTE_BGR: bOffs = new int[]{2, 1, 0}; // NOTE: These are reversed from what the cm.createCompatibleWritableRaster would return return Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, width, height, width * 3, 3, bOffs, null); case BufferedImage.TYPE_4BYTE_ABGR: case BufferedImage.TYPE_4BYTE_ABGR_PRE: bOffs = new int[] {3, 2, 1, 0}; // NOTE: These are reversed from what the cm.createCompatibleWritableRaster would return return Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, width, height, width * 4, 4, bOffs, null); case BufferedImage.TYPE_CUSTOM: // Peek into the sample model to see if we have a sample model that will be incompatible with the default case SampleModel sm = pOriginal.getRaster().getSampleModel(); if (sm instanceof ComponentSampleModel) { bOffs = ((ComponentSampleModel) sm).getBandOffsets(); return Raster.createInterleavedRaster(sm.getDataType(), width, height, width * bOffs.length, bOffs.length, bOffs, null); } // Else fall through default: return pOriginal.getColorModel().createCompatibleWritableRaster(width, height); } } return pModel.createCompatibleWritableRaster(width, height); } /** * Converts the {@code Image} to a {@code BufferedImage} of the given type. * The new image will have the same {@code ColorModel}, {@code Raster} and * properties as the original image, if possible. ** If the image is already a {@code BufferedImage} of the given type, it * is simply returned and no conversion takes place. *
* * @param pOriginal the image to convert. * @param pType the type of buffered image * * @return a {@code BufferedImage} * * @throws IllegalArgumentException if {@code pOriginal == null} * or if {@code pType} is not a valid type for {@code BufferedImage} * * @see java.awt.image.BufferedImage#getType() */ public static BufferedImage toBuffered(Image pOriginal, int pType) { return toBuffered(pOriginal, pType, null); } /** * * @param pOriginal the original image * @param pType the type of {@code BufferedImage} to create * @param pICM the optional {@code IndexColorModel} to use. If not * {@code null} the {@code pType} must be compatible with the color model * @return a {@code BufferedImage} * @throws IllegalArgumentException if {@code pType} is not compatible with * the color model */ private static BufferedImage toBuffered(Image pOriginal, int pType, IndexColorModel pICM) { // Don't convert if it already is BufferedImage and correct type if ((pOriginal instanceof BufferedImage) && ((BufferedImage) pOriginal).getType() == pType && (pICM == null || equals(((BufferedImage) pOriginal).getColorModel(), pICM))) { return (BufferedImage) pOriginal; } if (pOriginal == null) { throw new IllegalArgumentException("original == null"); } //System.out.println("--> Doing full BufferedImage conversion, using Graphics.drawImage()."); // Create a buffered image // NOTE: The getWidth and getHeight methods, will wait for the image BufferedImage image; if (pICM == null) { image = createBuffered(getWidth(pOriginal), getHeight(pOriginal), pType, Transparency.TRANSLUCENT);//new BufferedImage(getWidth(pOriginal), getHeight(pOriginal), pType); } else { image = new BufferedImage(getWidth(pOriginal), getHeight(pOriginal), pType, pICM); } // Draw the image onto the buffer drawOnto(image, pOriginal); return image; } /** * Draws the source image onto the buffered image, using * {@code AlphaComposite.Src} and coordinates {@code 0, 0}. * * @param pDestination the image to draw on * @param pSource the source image to draw * * @throws NullPointerException if {@code pDestination} or {@code pSource} is {@code null} */ static void drawOnto(final BufferedImage pDestination, final Image pSource) { Graphics2D g = pDestination.createGraphics(); try { g.setComposite(AlphaComposite.Src); g.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_DISABLE); g.drawImage(pSource, 0, 0, null); } finally { g.dispose(); } } /** * Creates a flipped version of the given image. * * @param pImage the image to flip * @param pAxis the axis to flip around * @return a new {@code BufferedImage} */ public static BufferedImage createFlipped(final Image pImage, final int pAxis) { switch (pAxis) { case FLIP_HORIZONTAL: case FLIP_VERTICAL: // TODO case FLIP_BOTH:?? same as rotate 180? break; default: throw new IllegalArgumentException("Illegal direction: " + pAxis); } BufferedImage source = toBuffered(pImage); AffineTransform transform; if (pAxis == FLIP_HORIZONTAL) { transform = AffineTransform.getTranslateInstance(0, source.getHeight()); transform.scale(1, -1); } else { transform = AffineTransform.getTranslateInstance(source.getWidth(), 0); transform.scale(-1, 1); } AffineTransformOp transformOp = new AffineTransformOp(transform, AffineTransformOp.TYPE_NEAREST_NEIGHBOR); return transformOp.filter(source, null); } /** * Rotates the image 90 degrees, clockwise (aka "rotate right"), * counter-clockwise (aka "rotate left") or 180 degrees, depending on the * {@code pDirection} argument. ** The new image will be completely covered with pixels from the source * image. *
* * @param pImage the source image. * @param pDirection the direction, must be either {@link #ROTATE_90_CW}, * {@link #ROTATE_90_CCW} or {@link #ROTATE_180} * * @return a new {@code BufferedImage} * */ public static BufferedImage createRotated(final Image pImage, final int pDirection) { switch (pDirection) { case ROTATE_90_CW: case ROTATE_90_CCW: case ROTATE_180: return createRotated(pImage, Math.toRadians(pDirection)); default: throw new IllegalArgumentException("Illegal direction: " + pDirection); } } /** * Rotates the image to the given angle. Areas not covered with pixels from * the source image will be left transparent, if possible. * * @param pImage the source image * @param pAngle the angle of rotation, in radians * * @return a new {@code BufferedImage}, unless {@code pAngle == 0.0} */ public static BufferedImage createRotated(final Image pImage, final double pAngle) { return createRotated0(toBuffered(pImage), pAngle); } private static BufferedImage createRotated0(final BufferedImage pSource, final double pAngle) { if ((Math.abs(Math.toDegrees(pAngle)) % 360) == 0) { return pSource; } final boolean fast = ((Math.abs(Math.toDegrees(pAngle)) % 90) == 0.0); final int w = pSource.getWidth(); final int h = pSource.getHeight(); // Compute new width and height double sin = Math.abs(Math.sin(pAngle)); double cos = Math.abs(Math.cos(pAngle)); int newW = (int) Math.floor(w * cos + h * sin); int newH = (int) Math.floor(h * cos + w * sin); AffineTransform transform = AffineTransform.getTranslateInstance((newW - w) / 2.0, (newH - h) / 2.0); transform.rotate(pAngle, w / 2.0, h / 2.0); // TODO: Figure out if this is correct BufferedImage dest = createTransparent(newW, newH); // See: http://weblogs.java.net/blog/campbell/archive/2007/03/java_2d_tricker_1.html Graphics2D g = dest.createGraphics(); try { g.transform(transform); if (!fast) { // Max quality g.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY); g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g.setPaint(new TexturePaint(pSource, new Rectangle2D.Float(0, 0, pSource.getWidth(), pSource.getHeight()))); g.fillRect(0, 0, pSource.getWidth(), pSource.getHeight()); } else { g.drawImage(pSource, 0, 0, null); } } finally { g.dispose(); } return dest; } /** * Creates a scaled instance of the given {@code Image}, and converts it to * a {@code BufferedImage} if needed. * If the original image is a {@code BufferedImage} the result will have * same type and color model. Note that this implies overhead, and is * probably not useful for anything but {@code IndexColorModel} images. * * @param pImage the {@code Image} to scale * @param pWidth width in pixels * @param pHeight height in pixels * @param pHints scaling ints * * @return a {@code BufferedImage} * * @throws NullPointerException if {@code pImage} is {@code null}. * * @see #createResampled(java.awt.Image, int, int, int) * @see Image#getScaledInstance(int,int,int) * @see Image#SCALE_AREA_AVERAGING * @see Image#SCALE_DEFAULT * @see Image#SCALE_FAST * @see Image#SCALE_REPLICATE * @see Image#SCALE_SMOOTH */ public static BufferedImage createScaled(Image pImage, int pWidth, int pHeight, int pHints) { ColorModel cm; int type = BI_TYPE_ANY; if (pImage instanceof RenderedImage) { cm = ((RenderedImage) pImage).getColorModel(); if (pImage instanceof BufferedImage) { type = ((BufferedImage) pImage).getType(); } } else { BufferedImageFactory factory = new BufferedImageFactory(pImage); cm = factory.getColorModel(); } BufferedImage scaled = createResampled(pImage, pWidth, pHeight, pHints); // Convert if color models or type differ, to behave as documented if (type != scaled.getType() && type != BI_TYPE_ANY || !equals(scaled.getColorModel(), cm)) { //System.out.print("Converting TYPE " + scaled.getType() + " -> " + type + "... "); //long start = System.currentTimeMillis(); WritableRaster raster; if (pImage instanceof BufferedImage) { raster = createCompatibleWritableRaster((BufferedImage) pImage, cm, pWidth, pHeight); } else { raster = cm.createCompatibleWritableRaster(pWidth, pHeight); } BufferedImage temp = new BufferedImage(cm, raster, cm.isAlphaPremultiplied(), null); if (cm instanceof IndexColorModel && pHints == Image.SCALE_SMOOTH) { // TODO: DiffusionDither does not support transparency at the moment, this will create bad results new DiffusionDither((IndexColorModel) cm).filter(scaled, temp); } else { drawOnto(temp, scaled); } scaled = temp; //long end = System.currentTimeMillis(); //System.out.println("Time: " + (end - start) + " ms"); } return scaled; } private static boolean equals(ColorModel pLeft, ColorModel pRight) { if (pLeft == pRight) { return true; } if (!pLeft.equals(pRight)) { return false; } // Now, the models are equal, according to the equals method // Test indexcolormodels for equality, the maps must be equal if (pLeft instanceof IndexColorModel) { IndexColorModel icm1 = (IndexColorModel) pLeft; IndexColorModel icm2 = (IndexColorModel) pRight; // NOTE: Safe, they're equal final int mapSize1 = icm1.getMapSize(); final int mapSize2 = icm2.getMapSize(); if (mapSize1 != mapSize2) { return false; } for (int i = 0; i < mapSize1; i++) { if (icm1.getRGB(i) != icm2.getRGB(i)) { return false; } } return true; } return true; } /** * Creates a scaled instance of the given {@code Image}, and converts it to * a {@code BufferedImage} if needed. * * @param pImage the {@code Image} to scale * @param pWidth width in pixels * @param pHeight height in pixels * @param pHints scaling mHints * * @return a {@code BufferedImage} * * @throws NullPointerException if {@code pImage} is {@code null}. * * @see Image#SCALE_AREA_AVERAGING * @see Image#SCALE_DEFAULT * @see Image#SCALE_FAST * @see Image#SCALE_REPLICATE * @see Image#SCALE_SMOOTH * @see ResampleOp */ public static BufferedImage createResampled(Image pImage, int pWidth, int pHeight, int pHints) { // NOTE: TYPE_4BYTE_ABGR or TYPE_3BYTE_BGR is more efficient when accelerated... BufferedImage image = pImage instanceof BufferedImage ? (BufferedImage) pImage : toBuffered(pImage, BufferedImage.TYPE_4BYTE_ABGR); return createResampled(image, pWidth, pHeight, pHints); } /** * Creates a scaled instance of the given {@code RenderedImage}, and * converts it to a {@code BufferedImage} if needed. * * @param pImage the {@code RenderedImage} to scale * @param pWidth width in pixels * @param pHeight height in pixels * @param pHints scaling mHints * * @return a {@code BufferedImage} * * @throws NullPointerException if {@code pImage} is {@code null}. * * @see Image#SCALE_AREA_AVERAGING * @see Image#SCALE_DEFAULT * @see Image#SCALE_FAST * @see Image#SCALE_REPLICATE * @see Image#SCALE_SMOOTH * @see ResampleOp */ public static BufferedImage createResampled(RenderedImage pImage, int pWidth, int pHeight, int pHints) { // NOTE: TYPE_4BYTE_ABGR or TYPE_3BYTE_BGR is more efficient when accelerated... BufferedImage image = pImage instanceof BufferedImage ? (BufferedImage) pImage : toBuffered(pImage, pImage.getColorModel().hasAlpha() ? BufferedImage.TYPE_4BYTE_ABGR : BufferedImage.TYPE_3BYTE_BGR); return createResampled(image, pWidth, pHeight, pHints); } /** * Creates a scaled instance of the given {@code BufferedImage}. * * @param pImage the {@code BufferedImage} to scale * @param pWidth width in pixels * @param pHeight height in pixels * @param pHints scaling mHints * * @return a {@code BufferedImage} * * @throws NullPointerException if {@code pImage} is {@code null}. * * @see Image#SCALE_AREA_AVERAGING * @see Image#SCALE_DEFAULT * @see Image#SCALE_FAST * @see Image#SCALE_REPLICATE * @see Image#SCALE_SMOOTH * @see ResampleOp */ public static BufferedImage createResampled(BufferedImage pImage, int pWidth, int pHeight, int pHints) { // Hints are converted between java.awt.Image hints and filter types return new ResampleOp(pWidth, pHeight, convertAWTHints(pHints)).filter(pImage, null); } private static int convertAWTHints(int pHints) { switch (pHints) { case Image.SCALE_FAST: case Image.SCALE_REPLICATE: return ResampleOp.FILTER_POINT; case Image.SCALE_AREA_AVERAGING: return ResampleOp.FILTER_BOX; //return ResampleOp.FILTER_CUBIC; case Image.SCALE_SMOOTH: return ResampleOp.FILTER_LANCZOS; default: //return ResampleOp.FILTER_TRIANGLE; return ResampleOp.FILTER_QUADRATIC; } } /** * Extracts an {@code IndexColorModel} from the given image. * * @param pImage the image to get the color model from * @param pColors the maximum number of colors in the resulting color model * @param pHints hints controlling transparency and color selection * * @return the extracted {@code IndexColorModel} * * @see #COLOR_SELECTION_DEFAULT * @see #COLOR_SELECTION_FAST * @see #COLOR_SELECTION_QUALITY * @see #TRANSPARENCY_DEFAULT * @see #TRANSPARENCY_OPAQUE * @see #TRANSPARENCY_BITMASK * @see #TRANSPARENCY_TRANSLUCENT */ public static IndexColorModel getIndexColorModel(Image pImage, int pColors, int pHints) { return IndexImage.getIndexColorModel(pImage, pColors, pHints); } /** * Creates an indexed version of the given image (a {@code BufferedImage} * with an {@code IndexColorModel}. * The resulting image will have a maximum of 256 different colors. * Transparent parts of the original will be replaced with solid black. * Default (possibly HW accelerated) dither will be used. * * @param pImage the image to convert * * @return an indexed version of the given image */ public static BufferedImage createIndexed(Image pImage) { return IndexImage.getIndexedImage(toBuffered(pImage), 256, Color.black, IndexImage.DITHER_DEFAULT); } /** * Creates an indexed version of the given image (a {@code BufferedImage} * with an {@code IndexColorModel}. * * @param pImage the image to convert * @param pColors number of colors in the resulting image * @param pMatte color to replace transparent parts of the original. * @param pHints hints controlling dither, transparency and color selection * * @return an indexed version of the given image * * @see #COLOR_SELECTION_DEFAULT * @see #COLOR_SELECTION_FAST * @see #COLOR_SELECTION_QUALITY * @see #DITHER_NONE * @see #DITHER_DEFAULT * @see #DITHER_DIFFUSION * @see #DITHER_DIFFUSION_ALTSCANS * @see #TRANSPARENCY_DEFAULT * @see #TRANSPARENCY_OPAQUE * @see #TRANSPARENCY_BITMASK * @see #TRANSPARENCY_TRANSLUCENT */ public static BufferedImage createIndexed(Image pImage, int pColors, Color pMatte, int pHints) { return IndexImage.getIndexedImage(toBuffered(pImage), pColors, pMatte, pHints); } /** * Creates an indexed version of the given image (a {@code BufferedImage} * with an {@code IndexColorModel}. * * @param pImage the image to convert * @param pColors the {@code IndexColorModel} to be used in the resulting * image. * @param pMatte color to replace transparent parts of the original. * @param pHints hints controlling dither, transparency and color selection * * @return an indexed version of the given image * * @see #COLOR_SELECTION_DEFAULT * @see #COLOR_SELECTION_FAST * @see #COLOR_SELECTION_QUALITY * @see #DITHER_NONE * @see #DITHER_DEFAULT * @see #DITHER_DIFFUSION * @see #DITHER_DIFFUSION_ALTSCANS * @see #TRANSPARENCY_DEFAULT * @see #TRANSPARENCY_OPAQUE * @see #TRANSPARENCY_BITMASK * @see #TRANSPARENCY_TRANSLUCENT */ public static BufferedImage createIndexed(Image pImage, IndexColorModel pColors, Color pMatte, int pHints) { return IndexImage.getIndexedImage(toBuffered(pImage), pColors, pMatte, pHints); } /** * Creates an indexed version of the given image (a {@code BufferedImage} * with an {@code IndexColorModel}. * * @param pImage the image to convert * @param pColors an {@code Image} used to get colors from. If the image is * has an {@code IndexColorModel}, it will be uesd, otherwise an * {@code IndexColorModel} is created from the image. * @param pMatte color to replace transparent parts of the original. * @param pHints hints controlling dither, transparency and color selection * * @return an indexed version of the given image * * @see #COLOR_SELECTION_DEFAULT * @see #COLOR_SELECTION_FAST * @see #COLOR_SELECTION_QUALITY * @see #DITHER_NONE * @see #DITHER_DEFAULT * @see #DITHER_DIFFUSION * @see #DITHER_DIFFUSION_ALTSCANS * @see #TRANSPARENCY_DEFAULT * @see #TRANSPARENCY_OPAQUE * @see #TRANSPARENCY_BITMASK * @see #TRANSPARENCY_TRANSLUCENT */ public static BufferedImage createIndexed(Image pImage, Image pColors, Color pMatte, int pHints) { return IndexImage.getIndexedImage(toBuffered(pImage), IndexImage.getIndexColorModel(pColors, 255, pHints), pMatte, pHints); } /** * Sharpens an image using a convolution matrix. * The sharpen kernel used, is defined by the following 3 by 3 matrix: *| 0.0 | -0.3 | 0.0 |
| -0.3 | 2.2 | -0.3 |
| 0.0 | -0.3 | 0.0 |
* This is the same result returned as * {@code sharpen(pOriginal, 0.3f)}. *
* * @param pOriginal the BufferedImage to sharpen * * @return a new BufferedImage, containing the sharpened image. */ public static BufferedImage sharpen(BufferedImage pOriginal) { return convolve(pOriginal, SHARPEN_KERNEL, EDGE_REFLECT); } /** * Sharpens an image using a convolution matrix. * The sharpen kernel used, is defined by the following 3 by 3 matrix: *| 0.0 | -{@code pAmount} | 0.0 |
| -{@code pAmount} | *4.0 * {@code pAmount} + 1.0 | *-{@code pAmount} |
| 0.0 | -{@code pAmount} | 0.0 |