mirror of
https://github.com/haraldk/TwelveMonkeys.git
synced 2026-05-18 00:00:03 -04:00
2036 lines
78 KiB
Java
2036 lines
78 KiB
Java
/*
|
|
* 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 "TwelveMonkeys" 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 OWNER 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.
|
|
*
|
|
* @todo Split palette generation out, into ColorModel classes.
|
|
*
|
|
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
|
* @author last modified by $Author: haku $
|
|
* @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ImageUtil.java#3 $
|
|
*/
|
|
public final class ImageUtil {
|
|
|
|
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:
|
|
* <TABLE border="1" cellspacing="0">
|
|
* <TR><TD>0.0</TD><TD>-0.3</TD><TD>0.0</TD></TR>
|
|
* <TR><TD>-0.3</TD><TD>2.2</TD><TD>-0.3</TD></TR>
|
|
* <TR><TD>0.0</TD><TD>-0.3</TD><TD>0.0</TD></TR>
|
|
* </TABLE>
|
|
*/
|
|
private static final Kernel SHARPEN_KERNEL = new Kernel(3, 3, SHARPEN_MATRIX);
|
|
|
|
/**
|
|
* Component that can be used with the MediaTracker etc.
|
|
*/
|
|
private static final Component NULL_COMPONENT = new Component() {};
|
|
|
|
/** Our static image tracker */
|
|
private static MediaTracker sTracker = new MediaTracker(NULL_COMPONENT);
|
|
//private static Object sTrackerMutex = new Object();
|
|
|
|
/** Image id used by the image tracker */
|
|
//private static int sTrackerId = 0;
|
|
|
|
/** */
|
|
protected static final AffineTransform IDENTITY_TRANSFORM = new AffineTransform();
|
|
/** */
|
|
protected static final Point LOCATION_UPPER_LEFT = new Point(0, 0);
|
|
|
|
/** */
|
|
private static final boolean COLORMODEL_TRANSFERTYPE_SUPPORTED = isColorModelTransferTypeSupported();
|
|
|
|
/** */
|
|
private static final GraphicsConfiguration DEFAULT_CONFIGURATION = getDefaultGraphicsConfiguration();
|
|
|
|
private static GraphicsConfiguration getDefaultGraphicsConfiguration() {
|
|
try {
|
|
GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment();
|
|
if (!env.isHeadlessInstance()) {
|
|
return env.getDefaultScreenDevice().getDefaultConfiguration();
|
|
}
|
|
}
|
|
catch (LinkageError e) {
|
|
// Means we are not in a 1.4+ VM, so skip testing for headless again
|
|
VM_SUPPORTS_ACCELERATION = false;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** Creates an ImageUtil. Private constructor. */
|
|
private ImageUtil() {
|
|
}
|
|
|
|
/**
|
|
* Tests if {@code ColorModel} has a {@code getTransferType} method.
|
|
*
|
|
* @return {@code true} if {@code ColorModel} has a
|
|
* {@code getTransferType} method
|
|
*/
|
|
private static boolean isColorModelTransferTypeSupported() {
|
|
try {
|
|
ColorModel.getRGBdefault().getTransferType();
|
|
return true;
|
|
}
|
|
catch (Throwable t) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Converts the {@code RenderedImage} to a {@code BufferedImage}.
|
|
* The new image will have the <em>same</em> {@code ColorModel},
|
|
* {@code Raster} and properties as the original image, if possible.
|
|
* <p/>
|
|
* If the image is allready 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 allready is a BufferedImage
|
|
if (pOriginal instanceof BufferedImage) {
|
|
return (BufferedImage) pOriginal;
|
|
}
|
|
if (pOriginal == null) {
|
|
throw new IllegalArgumentException("original == null");
|
|
}
|
|
|
|
// Copy properties
|
|
Hashtable<String, Object> properties;
|
|
String[] names = pOriginal.getPropertyNames();
|
|
if (names != null && names.length > 0) {
|
|
properties = new Hashtable<String, Object>(names.length);
|
|
|
|
for (String name : names) {
|
|
properties.put(name, pOriginal.getProperty(name));
|
|
}
|
|
}
|
|
else {
|
|
properties = null;
|
|
}
|
|
|
|
// NOTE: This is a workaround for the broken Batik '*Red' classes, that
|
|
// throw NPE if copyData(null) is used. This may actually be faster too.
|
|
// See RenderedImage#copyData / RenderedImage#getData
|
|
Raster data = pOriginal.getData();
|
|
WritableRaster raster;
|
|
if (data instanceof WritableRaster) {
|
|
raster = (WritableRaster) data;
|
|
}
|
|
else {
|
|
raster = data.createCompatibleWritableRaster();
|
|
raster = pOriginal.copyData(raster);
|
|
}
|
|
|
|
// Create buffered image
|
|
ColorModel colorModel = pOriginal.getColorModel();
|
|
return new BufferedImage(colorModel, raster,
|
|
colorModel.isAlphaPremultiplied(),
|
|
properties);
|
|
}
|
|
|
|
/**
|
|
* Converts the {@code RenderedImage} to a {@code BufferedImage} of the
|
|
* given type.
|
|
* <p/>
|
|
* If the image is allready 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 allready 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.
|
|
* <p/>
|
|
* If the image is allready a {@code BufferedImage} of the given type, it
|
|
* is simply returned and no conversion takes place.
|
|
* <p/>
|
|
* 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.
|
|
* <p/>
|
|
* If the image is allready 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 allready 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 copy of the given image. The image will have the same
|
|
* colormodel and raster type, but will not share image (pixel) data.
|
|
*
|
|
* @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(pImage, img);
|
|
|
|
return img;
|
|
}
|
|
|
|
/**
|
|
* Creates a {@code WritableRaster} for the given {@code ColorModel} and
|
|
* pixel data.
|
|
* <p/>
|
|
* 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 with more
|
|
* resource-friendly than using
|
|
* {@code ColorModel.createCompatibleWritableRaster(w, h)}.
|
|
* <p/>
|
|
* For unknown combinations, the method will fallback to using
|
|
* {@code ColorModel.createCompatibleWritableRaster(w, h)} and
|
|
* {@code WritableRaster.setDataElements(w, h, pixels)}
|
|
* <p/>
|
|
* Note that the {@code ColorModel} and pixel data are <em>not</em> 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 with the array from grabber.getPixels()
|
|
// directly, and creating a raster based on the 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 = data.length / (w * h);
|
|
bands = pColorModel.getNumComponents();
|
|
}
|
|
else if (pPixels instanceof short[]) {
|
|
short[] data = (short[]) pPixels;
|
|
buffer = new DataBufferUShort(data, data.length);
|
|
bands = data.length / (pWidth * pHeight);
|
|
//bands = cm.getNumComponents();
|
|
}
|
|
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);
|
|
}
|
|
|
|
//bands = pColorModel.getNumComponents();
|
|
//System.out.println("Pixels: " + data.length + " (" + buffer.getSize() + ")");
|
|
//System.out.println("w*h*bands: " + (pWidth * pHeight * bands));
|
|
//System.out.println("Bands: " + bands);
|
|
//System.out.println("Numcomponents: " + pColorModel.getNumComponents());
|
|
}
|
|
else {
|
|
//System.out.println("Fallback!");
|
|
// Fallback mode, slower & requires more memory, but compatible
|
|
bands = -1;
|
|
|
|
// Create raster from colormodel, w and h
|
|
raster = pColorModel.createCompatibleWritableRaster(pWidth, pHeight);
|
|
raster.setDataElements(0, 0, pWidth, pHeight, pPixels); // Note: This is known to throw ClassCastExceptions..
|
|
}
|
|
|
|
//System.out.println("Bands: " + bands);
|
|
//System.out.println("Pixels: " + pixels.getClass() + " length: " + buffer.getSize());
|
|
//System.out.println("Needed Raster: " + cm.createCompatibleWritableRaster(1, 1));
|
|
|
|
if (raster == null) {
|
|
//int bits = cm.getPixelSize();
|
|
//if (bits > 4) {
|
|
if (pColorModel instanceof IndexColorModel && isIndexedPacked((IndexColorModel) pColorModel)) {
|
|
//System.out.println("Creating packed indexed model");
|
|
raster = Raster.createPackedRaster(buffer, pWidth, pHeight, pColorModel.getPixelSize(), LOCATION_UPPER_LEFT);
|
|
}
|
|
else if (pColorModel instanceof PackedColorModel) {
|
|
//System.out.println("Creating packed model");
|
|
PackedColorModel pcm = (PackedColorModel) pColorModel;
|
|
raster = Raster.createPackedRaster(buffer, pWidth, pHeight, pWidth, pcm.getMasks(), LOCATION_UPPER_LEFT);
|
|
}
|
|
else {
|
|
//System.out.println("Creating interleaved model");
|
|
// (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);
|
|
}
|
|
//System.out.println("zzz Data array: " + buffer.getSize());
|
|
|
|
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 mWidth the requested width of the raster
|
|
* @param mHeight the requested height of the raster
|
|
*
|
|
* @return a new WritableRaster
|
|
*/
|
|
static WritableRaster createCompatibleWritableRaster(BufferedImage pOriginal, ColorModel pModel, int mWidth, int mHeight) {
|
|
if (pModel == null || equals(pOriginal.getColorModel(), pModel)) {
|
|
switch (pOriginal.getType()) {
|
|
case BufferedImage.TYPE_3BYTE_BGR:
|
|
int[] bOffs = {2, 1, 0}; // NOTE: These are reversed from what the cm.createCompatibleWritableRaster would return
|
|
return Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE,
|
|
mWidth, mHeight,
|
|
mWidth * 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,
|
|
mWidth, mHeight,
|
|
mWidth * 4, 4,
|
|
bOffs, null);
|
|
default:
|
|
return pOriginal.getColorModel().createCompatibleWritableRaster(mWidth, mHeight);
|
|
}
|
|
}
|
|
return pModel.createCompatibleWritableRaster(mWidth, mHeight);
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
* <p/>
|
|
* If the image is allready 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 allready 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.
|
|
* <p/>
|
|
* 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);
|
|
//AffineTransformOp transformOp = new AffineTransformOp(
|
|
// transform, fast ? AffineTransformOp.TYPE_NEAREST_NEIGHBOR : 3 // 3 == TYPE_BICUBIC
|
|
//);
|
|
//
|
|
//return transformOp.filter(pSource, null);
|
|
|
|
// TODO: Figure out if this is correct
|
|
BufferedImage dest = createTransparent(newW, newH);
|
|
//ColorModel cm = pSource.getColorModel();
|
|
//new BufferedImage(cm,
|
|
// createCompatibleWritableRaster(pSource, cm, newW, newH),
|
|
// cm.isAlphaPremultiplied(), null);
|
|
|
|
// 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) {
|
|
// Clear with all transparent
|
|
//Composite normal = g.getComposite();
|
|
//g.setComposite(AlphaComposite.Clear);
|
|
//g.fillRect(0, 0, newW, newH);
|
|
//g.setComposite(normal);
|
|
|
|
// 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 colormodel. 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 colormodels 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) {
|
|
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) {
|
|
// TODO: These conversions are broken!
|
|
// box == area average
|
|
// point == replicate (or..?)
|
|
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:
|
|
* <TABLE border="1" cellspacing="0">
|
|
* <TR><TD>0.0</TD><TD>-0.3</TD><TD>0.0</TD></TR>
|
|
* <TR><TD>-0.3</TD><TD>2.2</TD><TD>-0.3</TD></TR>
|
|
* <TR><TD>0.0</TD><TD>-0.3</TD><TD>0.0</TD></TR>
|
|
* </TABLE>
|
|
* <P/>
|
|
* 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:
|
|
* <TABLE border="1" cellspacing="0">
|
|
* <TR><TD>0.0</TD><TD>-{@code pAmmount}</TD><TD>0.0</TD></TR>
|
|
* <TR><TD>-{@code pAmmount}</TD>
|
|
* <TD>4.0 * {@code pAmmount} + 1.0</TD>
|
|
* <TD>-{@code pAmmount}</TD></TR>
|
|
* <TR><TD>0.0</TD><TD>-{@code pAmmount}</TD><TD>0.0</TD></TR>
|
|
* </TABLE>
|
|
*
|
|
* @param pOriginal the BufferedImage to sharpen
|
|
* @param pAmmount the ammount of sharpening
|
|
*
|
|
* @return a BufferedImage, containing the sharpened image.
|
|
*/
|
|
public static BufferedImage sharpen(BufferedImage pOriginal, float pAmmount) {
|
|
if (pAmmount == 0f) {
|
|
return pOriginal;
|
|
}
|
|
|
|
// Create the convolution matrix
|
|
float[] data = new float[] {
|
|
0.0f, -pAmmount, 0.0f, -pAmmount, 4f * pAmmount + 1f, -pAmmount, 0.0f, -pAmmount, 0.0f
|
|
};
|
|
|
|
// Do the filtering
|
|
return convolve(pOriginal, new Kernel(3, 3, data), EDGE_REFLECT);
|
|
}
|
|
|
|
/**
|
|
* Creates a blurred version of the given image.
|
|
*
|
|
* @param pOriginal the original image
|
|
*
|
|
* @return a new {@code BufferedImage} with a blurred version of the given image
|
|
*/
|
|
public static BufferedImage blur(BufferedImage pOriginal) {
|
|
return blur(pOriginal, 1.5f);
|
|
}
|
|
|
|
// Some work to do... Is okay now, for range 0...1, anything above creates
|
|
// artifacts.
|
|
// The idea here is that the sum of all terms in the matrix must be 1.
|
|
|
|
/**
|
|
* Creates a blurred version of the given image.
|
|
*
|
|
* @param pOriginal the original image
|
|
* @param pRadius the ammount to blur
|
|
*
|
|
* @return a new {@code BufferedImage} with a blurred version of the given image
|
|
*/
|
|
public static BufferedImage blur(BufferedImage pOriginal, float pRadius) {
|
|
if (pRadius <= 1f) {
|
|
return pOriginal;
|
|
}
|
|
|
|
// TODO: Re-implement using two-pass one-dimensional gaussion blur
|
|
// See: http://en.wikipedia.org/wiki/Gaussian_blur#Implementation
|
|
// Also see http://www.jhlabs.com/ip/blurring.html
|
|
|
|
// TODO: Rethink... Fixed ammount and scale matrix instead?
|
|
// pAmmount = 1f - pAmmount;
|
|
// float pAmmount = 1f - pRadius;
|
|
//
|
|
// // Normalize ammount
|
|
// float normAmt = (1f - pAmmount) / 24;
|
|
//
|
|
// // Create the convolution matrix
|
|
// float[] data = new float[] {
|
|
// normAmt / 2, normAmt, normAmt, normAmt, normAmt / 2,
|
|
// normAmt, normAmt, normAmt * 2, normAmt, normAmt,
|
|
// normAmt, normAmt * 2, pAmmount, normAmt * 2, normAmt,
|
|
// normAmt, normAmt, normAmt * 2, normAmt, normAmt,
|
|
// normAmt / 2, normAmt, normAmt, normAmt, normAmt / 2
|
|
// };
|
|
//
|
|
// // Do the filtering
|
|
// return convolve(pOriginal, new Kernel(5, 5, data), EDGE_REFLECT);
|
|
|
|
Kernel horizontal = makeKernel(pRadius);
|
|
Kernel vertical = new Kernel(horizontal.getHeight(), horizontal.getWidth(), horizontal.getKernelData(null));
|
|
|
|
BufferedImage temp = addBorder(pOriginal, horizontal.getWidth() / 2, vertical.getHeight() / 2, EDGE_REFLECT);
|
|
|
|
temp = convolve(temp, horizontal, EDGE_NO_OP);
|
|
temp = convolve(temp, vertical, EDGE_NO_OP);
|
|
|
|
return temp.getSubimage(
|
|
horizontal.getWidth() / 2, vertical.getHeight() / 2, pOriginal.getWidth(), pOriginal.getHeight()
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Make a Gaussian blur {@link Kernel}.
|
|
*
|
|
* @param radius the blur radius
|
|
* @return a new blur {@code Kernel}
|
|
*/
|
|
private static Kernel makeKernel(float radius) {
|
|
int r = (int) Math.ceil(radius);
|
|
int rows = r * 2 + 1;
|
|
float[] matrix = new float[rows];
|
|
float sigma = radius / 3;
|
|
float sigma22 = 2 * sigma * sigma;
|
|
float sigmaPi2 = (float) (2 * Math.PI * sigma);
|
|
float sqrtSigmaPi2 = (float) Math.sqrt(sigmaPi2);
|
|
float radius2 = radius * radius;
|
|
float total = 0;
|
|
int index = 0;
|
|
for (int row = -r; row <= r; row++) {
|
|
float distance = row * row;
|
|
if (distance > radius2) {
|
|
matrix[index] = 0;
|
|
}
|
|
else {
|
|
matrix[index] = (float) Math.exp(-(distance) / sigma22) / sqrtSigmaPi2;
|
|
}
|
|
total += matrix[index];
|
|
index++;
|
|
}
|
|
for (int i = 0; i < rows; i++) {
|
|
matrix[i] /= total;
|
|
}
|
|
|
|
return new Kernel(rows, 1, matrix);
|
|
}
|
|
|
|
|
|
/**
|
|
* Convolves an image, using a convolution matrix.
|
|
*
|
|
* @param pOriginal the BufferedImage to sharpen
|
|
* @param pKernel the kernel
|
|
* @param pEdgeOperation the edge operation. Must be one of {@link #EDGE_NO_OP},
|
|
* {@link #EDGE_ZERO_FILL}, {@link #EDGE_REFLECT} or {@link #EDGE_WRAP}
|
|
*
|
|
* @return a new BufferedImage, containing the sharpened image.
|
|
*/
|
|
public static BufferedImage convolve(BufferedImage pOriginal, Kernel pKernel, int pEdgeOperation) {
|
|
// Allow for 2 more edge operations
|
|
BufferedImage original;
|
|
switch (pEdgeOperation) {
|
|
case EDGE_REFLECT:
|
|
case EDGE_WRAP:
|
|
original = addBorder(pOriginal, pKernel.getWidth() / 2, pKernel.getHeight() / 2, pEdgeOperation);
|
|
break;
|
|
default:
|
|
original = pOriginal;
|
|
break;
|
|
}
|
|
|
|
// Create convolution operation
|
|
ConvolveOp convolve = new ConvolveOp(pKernel, pEdgeOperation, null);
|
|
|
|
// Workaround for what seems to be a Java2D bug:
|
|
// ConvolveOp needs explicit destination image type for some "uncommon"
|
|
// image types. However, TYPE_3BYTE_BGR is what javax.imageio.ImageIO
|
|
// normally returns for color JPEGs... :-/
|
|
BufferedImage result = null;
|
|
if (original.getType() == BufferedImage.TYPE_3BYTE_BGR) {
|
|
result = createBuffered(
|
|
pOriginal.getWidth(), pOriginal.getHeight(),
|
|
pOriginal.getType(), pOriginal.getColorModel().getTransparency()
|
|
);
|
|
}
|
|
|
|
// Do the filtering (if result is null, a new image will be created)
|
|
BufferedImage image = convolve.filter(original, result);
|
|
|
|
if (pOriginal != original) {
|
|
// Remove the border
|
|
image = image.getSubimage(
|
|
pKernel.getWidth() / 2, pKernel.getHeight() / 2, pOriginal.getWidth(), pOriginal.getHeight()
|
|
);
|
|
}
|
|
|
|
return image;
|
|
}
|
|
|
|
private static BufferedImage addBorder(final BufferedImage pOriginal, final int pBorderX, final int pBorderY, final int pEdgeOperation) {
|
|
// TODO: Might be faster if we could clone raster and strech it...
|
|
int w = pOriginal.getWidth();
|
|
int h = pOriginal.getHeight();
|
|
|
|
ColorModel cm = pOriginal.getColorModel();
|
|
WritableRaster raster = cm.createCompatibleWritableRaster(w + 2 * pBorderX, h + 2 * pBorderY);
|
|
BufferedImage bordered = new BufferedImage(cm, raster, cm.isAlphaPremultiplied(), null);
|
|
|
|
Graphics2D g = bordered.createGraphics();
|
|
try {
|
|
g.setComposite(AlphaComposite.Src);
|
|
g.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_DISABLE);
|
|
|
|
// Draw original in center
|
|
g.drawImage(pOriginal, pBorderX, pBorderY, null);
|
|
|
|
// TODO: I guess we need the top/left etc, if the corner pixels are covered by the kernel
|
|
switch (pEdgeOperation) {
|
|
case EDGE_REFLECT:
|
|
// Top/left (empty)
|
|
g.drawImage(pOriginal, pBorderX, 0, pBorderX + w, pBorderY, 0, 0, w, 1, null); // Top/center
|
|
// Top/right (empty)
|
|
|
|
g.drawImage(pOriginal, -w + pBorderX, pBorderY, pBorderX, h + pBorderY, 0, 0, 1, h, null); // Center/left
|
|
// Center/center (already drawn)
|
|
g.drawImage(pOriginal, w + pBorderX, pBorderY, 2 * pBorderX + w, h + pBorderY, w - 1, 0, w, h, null); // Center/right
|
|
|
|
// Bottom/left (empty)
|
|
g.drawImage(pOriginal, pBorderX, pBorderY + h, pBorderX + w, 2 * pBorderY + h, 0, h - 1, w, h, null); // Bottom/center
|
|
// Bottom/right (empty)
|
|
break;
|
|
case EDGE_WRAP:
|
|
g.drawImage(pOriginal, -w + pBorderX, -h + pBorderY, null); // Top/left
|
|
g.drawImage(pOriginal, pBorderX, -h + pBorderY, null); // Top/center
|
|
g.drawImage(pOriginal, w + pBorderX, -h + pBorderY, null); // Top/right
|
|
|
|
g.drawImage(pOriginal, -w + pBorderX, pBorderY, null); // Center/left
|
|
// Center/center (already drawn)
|
|
g.drawImage(pOriginal, w + pBorderX, pBorderY, null); // Center/right
|
|
|
|
g.drawImage(pOriginal, -w + pBorderX, h + pBorderY, null); // Bottom/left
|
|
g.drawImage(pOriginal, pBorderX, h + pBorderY, null); // Bottom/center
|
|
g.drawImage(pOriginal, w + pBorderX, h + pBorderY, null); // Bottom/right
|
|
break;
|
|
default:
|
|
throw new IllegalArgumentException("Illegal edge operation " + pEdgeOperation);
|
|
}
|
|
|
|
}
|
|
finally {
|
|
g.dispose();
|
|
}
|
|
|
|
//ConvolveTester.showIt(bordered, "jaffe");
|
|
|
|
return bordered;
|
|
}
|
|
|
|
/**
|
|
* Adds contrast
|
|
*
|
|
* @param pOriginal the BufferedImage to add contrast to
|
|
*
|
|
* @return an {@code Image}, containing the contrasted image.
|
|
*/
|
|
public static Image contrast(Image pOriginal) {
|
|
return contrast(pOriginal, 0.3f);
|
|
}
|
|
|
|
/**
|
|
* Changes the contrast of the image
|
|
*
|
|
* @param pOriginal the {@code Image} to change
|
|
* @param pAmmount the ammount of contrast in the range [-1.0..1.0].
|
|
*
|
|
* @return an {@code Image}, containing the contrasted image.
|
|
*/
|
|
public static Image contrast(Image pOriginal, float pAmmount) {
|
|
// No change, return original
|
|
if (pAmmount == 0f) {
|
|
return pOriginal;
|
|
}
|
|
|
|
// Create filter
|
|
RGBImageFilter filter = new BrightnessContrastFilter(0f, pAmmount);
|
|
|
|
// Return contrast adjusted image
|
|
return filter(pOriginal, filter);
|
|
}
|
|
|
|
|
|
/**
|
|
* Changes the brightness of the original image.
|
|
*
|
|
* @param pOriginal the {@code Image} to change
|
|
* @param pAmmount the ammount of brightness in the range [-2.0..2.0].
|
|
*
|
|
* @return an {@code Image}
|
|
*/
|
|
public static Image brightness(Image pOriginal, float pAmmount) {
|
|
// No change, return original
|
|
if (pAmmount == 0f) {
|
|
return pOriginal;
|
|
}
|
|
|
|
// Create filter
|
|
RGBImageFilter filter = new BrightnessContrastFilter(pAmmount, 0f);
|
|
|
|
// Return brightness adjusted image
|
|
return filter(pOriginal, filter);
|
|
}
|
|
|
|
|
|
/**
|
|
* Converts an image to grayscale.
|
|
*
|
|
* @see GrayFilter
|
|
* @see RGBImageFilter
|
|
*
|
|
* @param pOriginal the image to convert.
|
|
* @return a new Image, containing the gray image data.
|
|
*/
|
|
public static Image grayscale(Image pOriginal) {
|
|
// Create filter
|
|
RGBImageFilter filter = new GrayFilter();
|
|
|
|
// Convert to gray
|
|
return filter(pOriginal, filter);
|
|
}
|
|
|
|
/**
|
|
* Filters an image, using the given {@code ImageFilter}.
|
|
*
|
|
* @param pOriginal the original image
|
|
* @param pFilter the filter to apply
|
|
*
|
|
* @return the new {@code Image}
|
|
*/
|
|
public static Image filter(Image pOriginal, ImageFilter pFilter) {
|
|
// Create a filtered source
|
|
ImageProducer source = new FilteredImageSource(pOriginal.getSource(), pFilter);
|
|
|
|
// Create new image
|
|
return Toolkit.getDefaultToolkit().createImage(source);
|
|
}
|
|
|
|
/**
|
|
* Tries to use H/W-accellerated code for an image for display purposes.
|
|
* Note that transparent parts of the image might be replaced by solid
|
|
* color. Additional image information not used by the current diplay
|
|
* hardware may be discarded, like extra bith depth etc.
|
|
*
|
|
* @param pImage any {@code Image}
|
|
* @return a {@code BufferedImage}
|
|
*/
|
|
public static BufferedImage accelerate(Image pImage) {
|
|
return accelerate(pImage, null, DEFAULT_CONFIGURATION);
|
|
}
|
|
|
|
/**
|
|
* Tries to use H/W-accellerated code for an image for display purposes.
|
|
* Note that transparent parts of the image might be replaced by solid
|
|
* color. Additional image information not used by the current diplay
|
|
* hardware may be discarded, like extra bith depth etc.
|
|
*
|
|
* @param pImage any {@code Image}
|
|
* @param pConfiguration the {@code GraphicsConfiguration} to accelerate
|
|
* for
|
|
*
|
|
* @return a {@code BufferedImage}
|
|
*/
|
|
public static BufferedImage accelerate(Image pImage, GraphicsConfiguration pConfiguration) {
|
|
return accelerate(pImage, null, pConfiguration);
|
|
}
|
|
|
|
/**
|
|
* Tries to use H/W-accellerated code for an image for display purposes.
|
|
* Note that transparent parts of the image will be replaced by solid
|
|
* color. Additional image information not used by the current diplay
|
|
* hardware may be discarded, like extra bith depth etc.
|
|
*
|
|
* @param pImage any {@code Image}
|
|
* @param pBackgroundColor the background color to replace any transparent
|
|
* parts of the image.
|
|
* May be {@code null}, in such case the color is undefined.
|
|
* @param pConfiguration the graphics configuration
|
|
* May be {@code null}, in such case the color is undefined.
|
|
*
|
|
* @return a {@code BufferedImage}
|
|
*/
|
|
static BufferedImage accelerate(Image pImage, Color pBackgroundColor, GraphicsConfiguration pConfiguration) {
|
|
// Skip acceleration if the layout of the image and color model is already ok
|
|
if (pImage instanceof BufferedImage) {
|
|
BufferedImage buffered = (BufferedImage) pImage;
|
|
// TODO: What if the createCompatibleImage insist on TYPE_CUSTOM...? :-P
|
|
if (buffered.getType() != BufferedImage.TYPE_CUSTOM && equals(buffered.getColorModel(), pConfiguration.getColorModel(buffered.getTransparency()))) {
|
|
return buffered;
|
|
}
|
|
}
|
|
if (pImage == null) {
|
|
throw new IllegalArgumentException("image == null");
|
|
}
|
|
|
|
int w = ImageUtil.getWidth(pImage);
|
|
int h = ImageUtil.getHeight(pImage);
|
|
|
|
// Create accelerated version
|
|
BufferedImage temp = createClear(w, h, BI_TYPE_ANY, getTransparency(pImage), pBackgroundColor, pConfiguration);
|
|
drawOnto(temp, pImage);
|
|
|
|
return temp;
|
|
}
|
|
|
|
private static int getTransparency(Image pImage) {
|
|
if (pImage instanceof BufferedImage) {
|
|
BufferedImage bi = (BufferedImage) pImage;
|
|
return bi.getTransparency();
|
|
}
|
|
return Transparency.OPAQUE;
|
|
}
|
|
|
|
/**
|
|
* Creates a transparent image.
|
|
*
|
|
* @param pWidth the requested width of the image
|
|
* @param pHeight the requested height of the image
|
|
*
|
|
* @throws IllegalArgumentException if {@code pType} is not a valid type
|
|
* for {@code BufferedImage}
|
|
*
|
|
* @return the new image
|
|
*/
|
|
public static BufferedImage createTransparent(int pWidth, int pHeight) {
|
|
return createTransparent(pWidth, pHeight, BI_TYPE_ANY);
|
|
}
|
|
|
|
/**
|
|
* Creates a transparent image.
|
|
*
|
|
* @see BufferedImage#BufferedImage(int,int,int)
|
|
*
|
|
* @param pWidth the requested width of the image
|
|
* @param pHeight the requested height of the image
|
|
* @param pType the type of {@code BufferedImage} to create
|
|
*
|
|
* @throws IllegalArgumentException if {@code pType} is not a valid type
|
|
* for {@code BufferedImage}
|
|
*
|
|
* @return the new image
|
|
*/
|
|
public static BufferedImage createTransparent(int pWidth, int pHeight, int pType) {
|
|
// Create
|
|
BufferedImage image = createBuffered(pWidth, pHeight, pType, Transparency.TRANSLUCENT);
|
|
|
|
// Clear image with transparent alpha by drawing a rectangle
|
|
Graphics2D g = image.createGraphics();
|
|
try {
|
|
g.setComposite(AlphaComposite.Clear);
|
|
g.fillRect(0, 0, pWidth, pHeight);
|
|
}
|
|
finally {
|
|
g.dispose();
|
|
}
|
|
|
|
return image;
|
|
}
|
|
|
|
/**
|
|
* Creates a clear image with the given background color.
|
|
*
|
|
* @see BufferedImage#BufferedImage(int,int,int)
|
|
*
|
|
* @param pWidth the requested width of the image
|
|
* @param pHeight the requested height of the image
|
|
* @param pBackground the background color. The color may be translucent.
|
|
* May be {@code null}, in such case the color is undefined.
|
|
*
|
|
* @throws IllegalArgumentException if {@code pType} is not a valid type
|
|
* for {@code BufferedImage}
|
|
*
|
|
* @return the new image
|
|
*/
|
|
public static BufferedImage createClear(int pWidth, int pHeight, Color pBackground) {
|
|
return createClear(pWidth, pHeight, BI_TYPE_ANY, pBackground);
|
|
}
|
|
|
|
/**
|
|
* Creates a clear image with the given background color.
|
|
*
|
|
* @see BufferedImage#BufferedImage(int,int,int)
|
|
*
|
|
* @param pWidth the width of the image to create
|
|
* @param pHeight the height of the image to create
|
|
* @param pType the type of image to create (one of the constants from
|
|
* {@link BufferedImage} or {@link #BI_TYPE_ANY})
|
|
* @param pBackground the background color. The color may be translucent.
|
|
* May be {@code null}, in such case the color is undefined.
|
|
*
|
|
* @throws IllegalArgumentException if {@code pType} is not a valid type
|
|
* for {@code BufferedImage}
|
|
*
|
|
* @return the new image
|
|
*/
|
|
public static BufferedImage createClear(int pWidth, int pHeight, int pType, Color pBackground) {
|
|
return createClear(pWidth, pHeight, pType, Transparency.OPAQUE, pBackground, DEFAULT_CONFIGURATION);
|
|
}
|
|
|
|
static BufferedImage createClear(int pWidth, int pHeight, int pType, int pTransparency, Color pBackground, GraphicsConfiguration pConfiguration) {
|
|
// Create
|
|
int transparency = (pBackground != null) ? pBackground.getTransparency() : pTransparency;
|
|
BufferedImage image = createBuffered(pWidth, pHeight, pType, transparency, pConfiguration);
|
|
|
|
if (pBackground != null) {
|
|
// Clear image with clear color, by drawing a rectangle
|
|
Graphics2D g = image.createGraphics();
|
|
try {
|
|
g.setComposite(AlphaComposite.Src); // Allow color to be translucent
|
|
g.setColor(pBackground);
|
|
g.fillRect(0, 0, pWidth, pHeight);
|
|
}
|
|
finally {
|
|
g.dispose();
|
|
}
|
|
}
|
|
|
|
return image;
|
|
}
|
|
|
|
/**
|
|
* Creates a {@code BufferedImage} of the given size and type. If possible,
|
|
* uses accelerated versions of BufferedImage from GraphicsConfiguration.
|
|
*
|
|
* @param pWidth the width of the image to create
|
|
* @param pHeight the height of the image to create
|
|
* @param pType the type of image to create (one of the constants from
|
|
* {@link BufferedImage} or {@link #BI_TYPE_ANY})
|
|
* @param pTransparency the transparency type (from {@link Transparency})
|
|
*
|
|
* @return a {@code BufferedImage}
|
|
*/
|
|
private static BufferedImage createBuffered(int pWidth, int pHeight, int pType, int pTransparency) {
|
|
return createBuffered(pWidth, pHeight, pType, pTransparency, DEFAULT_CONFIGURATION);
|
|
}
|
|
|
|
static BufferedImage createBuffered(int pWidth, int pHeight, int pType, int pTransparency,
|
|
GraphicsConfiguration pConfiguration) {
|
|
if (VM_SUPPORTS_ACCELERATION && pType == BI_TYPE_ANY) {
|
|
GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment();
|
|
if (supportsAcceleration(env)) {
|
|
return getConfiguration(pConfiguration).createCompatibleImage(pWidth, pHeight, pTransparency);
|
|
}
|
|
}
|
|
|
|
return new BufferedImage(pWidth, pHeight, getImageType(pType, pTransparency));
|
|
}
|
|
|
|
private static GraphicsConfiguration getConfiguration(final GraphicsConfiguration pConfiguration) {
|
|
return pConfiguration != null ? pConfiguration : DEFAULT_CONFIGURATION;
|
|
}
|
|
|
|
private static int getImageType(int pType, int pTransparency) {
|
|
// TODO: Handle TYPE_CUSTOM?
|
|
if (pType != BI_TYPE_ANY) {
|
|
return pType;
|
|
}
|
|
else {
|
|
switch (pTransparency) {
|
|
case Transparency.OPAQUE:
|
|
return BufferedImage.TYPE_INT_RGB;
|
|
case Transparency.BITMASK:
|
|
case Transparency.TRANSLUCENT:
|
|
return BufferedImage.TYPE_INT_ARGB;
|
|
default:
|
|
throw new IllegalArgumentException("Unknown transparency type: " + pTransparency);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Tests if the given {@code GraphicsEnvironment} supports accelleration
|
|
*
|
|
* @param pEnv the environment
|
|
* @return {@code true} if the {@code GraphicsEnvironment} supports
|
|
* acceleration
|
|
*/
|
|
private static boolean supportsAcceleration(GraphicsEnvironment pEnv) {
|
|
try {
|
|
// Acceleration only supported in non-headless environments, on 1.4+ VMs
|
|
return /*VM_SUPPORTS_ACCELERATION &&*/ !pEnv.isHeadlessInstance();
|
|
}
|
|
catch (LinkageError ignore) {
|
|
// Means we are not in a 1.4+ VM, so skip testing for headless again
|
|
VM_SUPPORTS_ACCELERATION = false;
|
|
}
|
|
|
|
// If the invocation fails, assume no accelleration is possible
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Gets the width of an Image.
|
|
* This method has the side-effect of completely loading the image.
|
|
*
|
|
* @param pImage an image.
|
|
*
|
|
* @return the width of the image, or -1 if the width could not be
|
|
* determined (i.e. an error occured while waiting for the
|
|
* image to load).
|
|
*/
|
|
public static int getWidth(Image pImage) {
|
|
int width = pImage.getWidth(NULL_COMPONENT);
|
|
if (width < 0) {
|
|
if (!waitForImage(pImage)) {
|
|
return -1; // Error while waiting
|
|
}
|
|
width = pImage.getWidth(NULL_COMPONENT);
|
|
}
|
|
|
|
return width;
|
|
}
|
|
|
|
/**
|
|
* Gets the height of an Image.
|
|
* This method has the side-effect of completely loading the image.
|
|
*
|
|
* @param pImage an image.
|
|
*
|
|
* @return the height of the image, or -1 if the height could not be
|
|
* determined (i.e. an error occured while waiting for the
|
|
* image to load).
|
|
*/
|
|
public static int getHeight(Image pImage) {
|
|
int height = pImage.getHeight(NULL_COMPONENT);
|
|
if (height < 0) {
|
|
if (!waitForImage(pImage)) {
|
|
return -1; // Error while waiting
|
|
}
|
|
height = pImage.getHeight(NULL_COMPONENT);
|
|
}
|
|
|
|
return height;
|
|
}
|
|
|
|
/**
|
|
* Waits for an image to load completely.
|
|
* Will wait forever.
|
|
*
|
|
* @param pImage an Image object to wait for.
|
|
*
|
|
* @return true if the image was loaded successfully, false if an error
|
|
* occured, or the wait was interrupted.
|
|
*
|
|
* @see #waitForImage(Image,long)
|
|
*/
|
|
public static boolean waitForImage(Image pImage) {
|
|
return waitForImages(new Image[]{pImage}, -1L);
|
|
}
|
|
|
|
/**
|
|
* Waits for an image to load completely.
|
|
* Will wait the specified time.
|
|
*
|
|
* @param pImage an Image object to wait for.
|
|
* @param pTimeOut the time to wait, in milliseconds.
|
|
*
|
|
* @return true if the image was loaded successfully, false if an error
|
|
* occured, or the wait was interrupted.
|
|
*
|
|
* @see #waitForImages(Image[],long)
|
|
*/
|
|
public static boolean waitForImage(Image pImage, long pTimeOut) {
|
|
return waitForImages(new Image[]{pImage}, pTimeOut);
|
|
}
|
|
|
|
/**
|
|
* Waits for a number of images to load completely.
|
|
* Will wait forever.
|
|
*
|
|
* @param pImages an array of Image objects to wait for.
|
|
*
|
|
* @return true if the images was loaded successfully, false if an error
|
|
* occured, or the wait was interrupted.
|
|
*
|
|
* @see #waitForImages(Image[],long)
|
|
*/
|
|
public static boolean waitForImages(Image[] pImages) {
|
|
return waitForImages(pImages, -1L);
|
|
}
|
|
|
|
/**
|
|
* Waits for a number of images to load completely.
|
|
* Will wait the specified time.
|
|
*
|
|
* @param pImages an array of Image objects to wait for
|
|
* @param pTimeOut the time to wait, in milliseconds
|
|
*
|
|
* @return true if the images was loaded successfully, false if an error
|
|
* occured, or the wait was interrupted.
|
|
*/
|
|
public static boolean waitForImages(Image[] pImages, long pTimeOut) {
|
|
// TODO: Need to make sure that we don't wait for the same image many times
|
|
// Use hashcode as id? Don't remove images from tracker? Hmmm...
|
|
boolean success = true;
|
|
|
|
// Create a local id for use with the mediatracker
|
|
int imageId;
|
|
|
|
// NOTE: The synchronization throws IllegalMonitorStateException if
|
|
// using JIT on J2SE 1.2 (tested version Sun JRE 1.2.2_017).
|
|
// Works perfectly interpreted... Hmmm...
|
|
//synchronized (sTrackerMutex) {
|
|
//imageId = ++sTrackerId;
|
|
//}
|
|
|
|
// NOTE: This is very experimental...
|
|
imageId = pImages.length == 1 ? System.identityHashCode(pImages[0]) : System.identityHashCode(pImages);
|
|
|
|
// Add images to tracker
|
|
for (Image image : pImages) {
|
|
sTracker.addImage(image, imageId);
|
|
|
|
// Start loading immediately
|
|
if (sTracker.checkID(imageId, false)) {
|
|
// Image is done, so remove again
|
|
sTracker.removeImage(image, imageId);
|
|
}
|
|
}
|
|
|
|
try {
|
|
if (pTimeOut < 0L) {
|
|
// Just wait
|
|
sTracker.waitForID(imageId);
|
|
}
|
|
else {
|
|
// Wait until timeout
|
|
// NOTE: waitForID(int, long) return value is undocumented.
|
|
// I assume that it returns true, if the image(s) loaded
|
|
// successfully before the timeout, however, I always check
|
|
// isErrorID later on, just in case...
|
|
success = sTracker.waitForID(imageId, pTimeOut);
|
|
}
|
|
}
|
|
catch (InterruptedException ie) {
|
|
// Interrupted while waiting, image not loaded
|
|
success = false;
|
|
}
|
|
finally {
|
|
// Remove images from mediatracker
|
|
for (Image pImage : pImages) {
|
|
sTracker.removeImage(pImage, imageId);
|
|
}
|
|
}
|
|
|
|
// If the wait was successfull, and no errors were reported for the
|
|
// images, return true
|
|
return success && !sTracker.isErrorID(imageId);
|
|
}
|
|
|
|
/**
|
|
* Tests wether the image has any transparent or semi-transparent pixels.
|
|
*
|
|
* @param pImage the image
|
|
* @param pFast if {@code true}, the method tests maximum 10 x 10 pixels,
|
|
* evenly spaced out in the image.
|
|
*
|
|
* @return {@code true} if transparent pixels are found, otherwise
|
|
* {@code false}.
|
|
*/
|
|
public static boolean hasTransparentPixels(RenderedImage pImage, boolean pFast) {
|
|
if (pImage == null) {
|
|
return false;
|
|
}
|
|
|
|
// First, test if the ColorModel supports alpha...
|
|
ColorModel cm = pImage.getColorModel();
|
|
if (!cm.hasAlpha()) {
|
|
return false;
|
|
}
|
|
|
|
if (cm.getTransparency() != Transparency.BITMASK
|
|
&& cm.getTransparency() != Transparency.TRANSLUCENT) {
|
|
return false;
|
|
}
|
|
|
|
// ... if so, test the pixels of the image hard way
|
|
Object data = null;
|
|
|
|
// Loop over tiles (noramally, BufferedImages have only one)
|
|
for (int yT = pImage.getMinTileY(); yT < pImage.getNumYTiles(); yT++) {
|
|
for (int xT = pImage.getMinTileX(); xT < pImage.getNumXTiles(); xT++) {
|
|
// Test pixels of each tile
|
|
Raster raster = pImage.getTile(xT, yT);
|
|
int xIncrement = pFast ? Math.max(raster.getWidth() / 10, 1) : 1;
|
|
int yIncrement = pFast ? Math.max(raster.getHeight() / 10, 1) : 1;
|
|
|
|
for (int y = 0; y < raster.getHeight(); y += yIncrement) {
|
|
for (int x = 0; x < raster.getWidth(); x += xIncrement) {
|
|
// Copy data for each pixel, without allocation array
|
|
data = raster.getDataElements(x, y, data);
|
|
|
|
// Test alpha value
|
|
if (cm.getAlpha(data) != 0xff) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Creates a translucent version of the given color.
|
|
*
|
|
* @param pColor the original color
|
|
* @param pTransparency the transparency level ({@code 0 - 255})
|
|
* @return a translucent color
|
|
*
|
|
* @throws NullPointerException if {@code pColor} is {@code null}
|
|
*/
|
|
public static Color createTranslucent(Color pColor, int pTransparency) {
|
|
//return new Color(pColor.getRed(), pColor.getGreen(), pColor.getBlue(), pTransparency);
|
|
return new Color(((pTransparency & 0xff) << 24) | (pColor.getRGB() & 0x00ffffff), true);
|
|
}
|
|
|
|
/**
|
|
* Blends two ARGB values half and half, to create a tone inbetween.
|
|
*
|
|
* @param pRGB1 color 1
|
|
* @param pRGB2 color 2
|
|
* @return the new rgb value
|
|
*/
|
|
static int blend(int pRGB1, int pRGB2) {
|
|
// Slightly modified from http://www.compuphase.com/graphic/scale3.htm
|
|
// to support alpha values
|
|
return (((pRGB1 ^ pRGB2) & 0xfefefefe) >> 1) + (pRGB1 & pRGB2);
|
|
}
|
|
|
|
/**
|
|
* Blends two colors half and half, to create a tone inbetween.
|
|
*
|
|
* @param pColor color 1
|
|
* @param pOther color 2
|
|
* @return a new {@code Color}
|
|
*/
|
|
public static Color blend(Color pColor, Color pOther) {
|
|
return new Color(blend(pColor.getRGB(), pOther.getRGB()), true);
|
|
|
|
/*
|
|
return new Color((pColor.getRed() + pOther.getRed()) / 2,
|
|
(pColor.getGreen() + pOther.getGreen()) / 2,
|
|
(pColor.getBlue() + pOther.getBlue()) / 2,
|
|
(pColor.getAlpha() + pOther.getAlpha()) / 2);
|
|
*/
|
|
}
|
|
|
|
/**
|
|
* Blends two colors, controlled by the blendfactor.
|
|
* A factor of {@code 0.0} will return the first color,
|
|
* a factor of {@code 1.0} will return the second.
|
|
*
|
|
* @param pColor color 1
|
|
* @param pOther color 2
|
|
* @param pBlendFactor {@code [0...1]}
|
|
* @return a new {@code Color}
|
|
*/
|
|
public static Color blend(Color pColor, Color pOther, float pBlendFactor) {
|
|
float inverseBlend = (1f - pBlendFactor);
|
|
return new Color(
|
|
clamp((pColor.getRed() * inverseBlend) + (pOther.getRed() * pBlendFactor)),
|
|
clamp((pColor.getGreen() * inverseBlend) + (pOther.getGreen() * pBlendFactor)),
|
|
clamp((pColor.getBlue() * inverseBlend) + (pOther.getBlue() * pBlendFactor)),
|
|
clamp((pColor.getAlpha() * inverseBlend) + (pOther.getAlpha() * pBlendFactor))
|
|
);
|
|
}
|
|
|
|
private static int clamp(float f) {
|
|
return (int) f;
|
|
}
|
|
/**
|
|
* PixelGrabber subclass that stores any potential properties from an image.
|
|
*/
|
|
/*
|
|
private static class MyPixelGrabber extends PixelGrabber {
|
|
private Hashtable mProps = null;
|
|
|
|
public MyPixelGrabber(Image pImage) {
|
|
// Simply grab all pixels, do not convert to default RGB space
|
|
super(pImage, 0, 0, -1, -1, false);
|
|
}
|
|
|
|
// Default implementation does not store the properties...
|
|
public void setProperties(Hashtable pProps) {
|
|
super.setProperties(pProps);
|
|
mProps = pProps;
|
|
}
|
|
|
|
public Hashtable getProperties() {
|
|
return mProps;
|
|
}
|
|
}
|
|
*/
|
|
|
|
/**
|
|
* Gets the transfer type from the given {@code ColorModel}.
|
|
* <p/>
|
|
* NOTE: This is a workaround for missing functionality in JDK 1.2.
|
|
*
|
|
* @param pModel the color model
|
|
* @return the transfer type
|
|
*
|
|
* @throws NullPointerException if {@code pModel} is {@code null}.
|
|
*
|
|
* @see java.awt.image.ColorModel#getTransferType()
|
|
*/
|
|
public static int getTransferType(ColorModel pModel) {
|
|
if (COLORMODEL_TRANSFERTYPE_SUPPORTED) {
|
|
return pModel.getTransferType();
|
|
}
|
|
else {
|
|
// Stupid workaround
|
|
// TODO: Create something that performs better
|
|
return pModel.createCompatibleSampleModel(1, 1).getDataType();
|
|
}
|
|
}
|
|
} |