com.day.image.Layer Maven / Gradle / Ivy
Show all versions of aem-sdk-api Show documentation
/*************************************************************************
*
* ADOBE CONFIDENTIAL
* __________________
*
* Copyright 2012 Adobe Systems Incorporated
* All Rights Reserved.
*
* NOTICE: All information contained herein is, and remains
* the property of Adobe Systems Incorporated and its suppliers,
* if any. The intellectual and technical concepts contained
* herein are proprietary to Adobe Systems Incorporated and its
* suppliers and are protected by trade secret or copyright law.
* Dissemination of this information or reproduction of this material
* is strictly forbidden unless prior written permission is obtained
* from Adobe Systems Incorporated.
**************************************************************************/
package com.day.image;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.TexturePaint;
import java.awt.geom.AffineTransform;
import java.awt.geom.Arc2D;
import java.awt.geom.Ellipse2D;
import java.awt.geom.GeneralPath;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BandCombineOp;
import java.awt.image.BufferedImage;
import java.awt.image.BufferedImageOp;
import java.awt.image.ColorModel;
import java.awt.image.ConvolveOp;
import java.awt.image.IndexColorModel;
import java.awt.image.Kernel;
import java.awt.image.RescaleOp;
import java.awt.image.WritableRaster;
import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import javax.imageio.IIOException;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.plugins.jpeg.JPEGImageWriteParam;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.ImageOutputStream;
import org.apache.commons.imaging.ImageReadException;
import org.apache.commons.imaging.Imaging;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.day.image.font.AbstractFont;
/**
* The Layer
class provides a simplified usage pattern for doing
* graphics rendering. As such the Layer
class forms the basis of
* Graphics Engine. The implementation is closely based on the
* BufferedImage
class of the Java2D API. For this reason the
* drawing idiom closely represents the Java2D API in which you first define the
* line style and paint mode of the object to be drawn in the second step.
*
* Classification of methods
*
*
*
* Layer modification
*
* The layer modifying operations change the complete layer and comprise
* operations such as rotating, blur, etc. The layer modifying operations are
* {@link #flatten}, {@link #rotate}, {@link #resize}, {@link #crop},
* {@link #emboss}, {@link #xForm}, {@link #colorize}, {@link #monotone},
* {@link #multitone}, {@link #blur}, {@link #sharpen}, {@link #xFormColors},
* {@link #replaceColor}, {@link #adjust}.
*
* Layer copying
*
* The layer copying operations copy parts - color channels or regions - or
* complete other layers onto/into the current layer. The layer copying
* operations comprise {@link #merge}, {@link #blit}, {@link #copyChannel},
* {@link #colorMask}
*
* Drawing modifiers
*
* The drawing modifiers modify the behaviour of subsequent drawing and
* filling operations in respect to paint, stroke, etc. The drawing modifiers
* are {@link #setPaint}, {@link #setStroke}, {@link #setLineStyle},
* {@link #setComposite}, {@link #setTransform} and {@link #setLuminanceSystem}.
*
* Drawing and filling
*
* The drawing and filling operations do just what they are meant to do :
* draw or fill geometrical figures or shapes. The drawing and filling
* operations comprise {@link #drawText}, {@link #fillRect}, {@link #drawRect},
* {@link #drawLine}, {@link #drawPolyLine}, {@link #drawEllipse},
* {@link #fillEllipse}, {@link #drawSegment}, {@link #drawSector},
* {@link #fillSector}, {@link #draw}, {@link #fill}.
*
* Miscellaneous
*
* These methods do miscellaneous operations not classifiable above :
* {@link #getPixel}, {@link #setPixel}, {@link #getBoundingBox},
* {@link #getBoundingBox}, {@link #getBackgroundColor}, {@link #floodFill},
* {@link #floodFill}
*
* Coordinate system
*
* Layer
coordinates are specified using the origin (0,0) in the
* top left corner, horizontally spanning to the right and vertically spanning
* down. This is the same usage as in the Java 2D API which is used to implement
* the Layer
class.
*
* The Java2D API support floating point coordinates, for this reason all the
* Layer
methods support floating point coordinates, too. This
* fact is of essential use in case of affine transformation applied to the
* drawing.
*
* Colors
*
* For each method that takes a numeric value as a color value - such as
* {@link #setPixel} - or returns a numeric color value - such as
* {@link #getPixel} - this number is a 32bit unsigned integer number, which
* encodes 8bits per color value and the alpha channel value. The colors are all
* defined to be RGB colors in the standard sRGB color space.
*
* Example: the value 0xff60c0f0 defines the alpha channel to 0xff, the
* red channel to 0x60, the green channel to 0xc0 and the blue channel to 0xf0.
*
* The methods taking a Paint
object as a parameter you may pass
* a Color
object which you define - amongst others - with the
* constructor taking the 32bit integer value encoded as noted above ;
* Paint col = new Color( 0xff6cc0f0, true );
*
*
* Properties of Layer
objects
*
*
* x, y, width, height
*
* The top, left corner of the layer as well as the width and height of the
* layer. The top, left corner coordinate is only used when merging or
* blitting layers, to support relative positioning of the layers. Also
* when writing the layer in GIF format, the top, left corner values are
* used.
*
* bgcolor
*
* The background color of the layer. The layer is filled with this color
* when being allocated. This is also the default color to flatten against.
*
* transparency
*
* The color in the image which is considered to be the transparent color.
* This value is only of use for GIF images, as JPG and PNG correctly
* account for the alpha channel values per pixel and create partially
* transparent pixels.
*
* opacity
*
* The opacity is used during layer merging to merge layers in partially
* transparent manner.
*
* mimeType
*
* This is the default MIME type of the layer. This is set from the image
* file if the layer is loaded from an image or defaults to
* "image/gif
". This is used to write the image, if the
* image type parameter is missing.
*
* quality
*
* Will define some quality values in the future. This is not used yet.
*
*
*
*
* Notes on J2SE 1.3
*
* The Graphics Engine uses the new ImageIO API to read and write image
* files. Unfortunately this API is only avaiable with J2SE 1.4 though for some
* time an early access version called EA2 has been available for J2SE 1.3.
* The Graphics Engine library contains this EA2 ImageIO implementation
* which is only used if running in J2SE 1.3.
*
* J2SE 1.3 contains a bug which prevents graphical applications from running
* correctly on Unix systems if no XServer is available. This is known as
* 'headless' operation mode and reported to Sun in
*
* Sun bug #4281163, support "headless" Java. The workaround is to have the
* PJA toolkit installed and
* starting the JVM with the following additional parameters :
*
* -Xbootclasspath/a:<lib>\pja.jar -Dawt.toolkit=com.eteks.awt.PJAToolkit \
* -Djava.awt.graphicsenv=com.eteks.java2d.PJAGraphicsEnvironment \
* -Djava2d.font.usePlatformFont=false
*
*
* @author fmeschbe
* @since coati
* @audience wad
*/
public class Layer {
/**
* default logger
*/
private static final Logger log = LoggerFactory.getLogger(Layer.class);
/**
* Initializes the {@link ImageSupport} class to (1) register the Gif image
* writer with the ImageIO but also to load the correct JRE release
* dependent classes.
*/
static {
ImageSupport.initialize();
}
// ---------- public constants
// ----------------------------------------------
/**
* Luminance factors for RGB in a Gamma 2.2 color space. Used for
* grayscaling. These values constitute the luminance vector with NTSC
* weights which are also used in the Communiqu� 2 Layer host object.
*
* @see Matrix
* Operations for Image Processing, Converting to Luminance
*/
public static final LuminanceSystem GAMMA22 = new LuminanceSystem(
"Gamma 2.2", .229f, .587f, .114f);
/**
* Luminance factors for RGB in a linear color space. Used for grayscaling.
*
* @see Matrix
* Operations for Image Processing, Converting to Luminance
*/
public static final LuminanceSystem LINEAR = new LuminanceSystem("Linear",
.3086f, .6094f, .0820f);
/**
* Luminance factors for RGB in a color space for contemporary CRT phosphors
* according to Rec. 709 ( ITU-R Recommendation BT.709, Basic Parameter
* Values for the HDTV Standard for the Studio and for International
* Programme Exchange (1990)). Used for grayscaling.
*
* @see
* 9. What weighting of red, green and blue corresponds to brightness?
*/
public static final LuminanceSystem REC709 = new LuminanceSystem("Rec709",
.2125f, .7154f, .0721f);
/**
* Default MIME type for writing the image. This value is only used, if it
* could not be deduced from the image (e.g. when loading a picture) or as
* parameter to the write
methods.
*/
public static final String DEFAULT_MIME_TYPE = "image/gif";
/**
* Channel number to identify the red color channel for diverse methods
* involving color channels, i.e. {@link #copyChannel(Layer, int, int)}
*/
public static final int RED_CHANNEL_ID = 0;
/**
* Channel number to identify the green color channel for diverse methods
* involving color channels, i.e. {@link #copyChannel(Layer, int, int)}
*/
public static final int GREEN_CHANNEL_ID = 1;
/**
* Channel number to identify the blue color channel for diverse methods
* involving color channels, i.e. {@link #copyChannel(Layer, int, int)}
*/
public static final int BLUE_CHANNEL_ID = 2;
/**
* Channel number to identify the alpha channel for diverse methods
* involving color channels, i.e. {@link #copyChannel(Layer, int, int)}
*/
public static final int ALPHA_CHANNEL_ID = 3;
// ---------- private constants
// ---------------------------------------------
/** Default left edge of the layer if not specified to the constructor. */
private static final int DEFAULT_IMAGE_ORIGINX = 0;
/** Default top edge of the layer if not specified to the constructor. */
private static final int DEFAULT_IMAGE_ORIGINY = 0;
/** Default width of the layer if not specified to the constructor. */
private static final int DEFAULT_IMAGE_WIDTH = 1;
/** Default height of the layer if not specified to the constructor. */
private static final int DEFAULT_IMAGE_HEIGHT = 1;
/**
* Default background color of the layer if not specified to the
* constructor. Due to popular demand, this is opqaue black and not
* transparent black ;-) note by t: for backward compatibility to cq2, this
* must be transparent black. i don't know, who this 'popular' is :-). i
* uncommeted this, to make sure, no other function uses this. they should
* use the transparent_image_background below.
*/
// protected static final Color DEFAULT_IMAGE_BACKGROUND = Color.black;
/** The transparent background color for various operations */
protected static final Color TRANSPARENT_IMAGE_BACKGROUND = new Color(0, 0,
0, 0);
/** Default opacity of the layer. Set by the constructor. */
private static final float DEFAULT_IMAGE_OPACITY = 1.0f;
/** Default Composite
of the Layer. Set by the constructor */
private static final Composite DEFAULT_LAYER_COMPOSITE = AlphaComposite.SrcOver;
/**
* Java 2D API image type used for internal drawing operations. This is the
* same image model used as in Communiqu� 2 : three color channels for red,
* green and blue plus an alpha channel for opacity.
*/
static final int IMAGE_TYPE = BufferedImage.TYPE_INT_ARGB;
/** The identity affine transform */
private static final AffineTransform IDENTITY_XFORM = new AffineTransform();
/**
* The system rendering hints. These hints will be set at first use in
* {@link #setRenderingHints(Graphics2D)}.
*/
private static Map RENDERING_HINTS = null;
/**
* the maximum target size to use subsampling while loading the image.
*/
private static final int MAX_SUBSAMPLING_SIZE = 1280;
// ---------- fields
// --------------------------------------------------------
/**
* Whether the {@link #baseImg} is in RGBA color model.
*/
private boolean baseImgIsRGBA;
/**
* This is the image we draw our stuff into. It is allocated by the
* constructors, but may be replaced by several layer operations, such as
* {@link #resize(int, int)}.
*
* This field must not be used to get the image underlying this
* Layer. Rather the {@link #getImage()} method should be used to get
* the image in whatever color space it was loaded from or created
* with or the {@link #getImageRGBA()} to get the image in the RGBA
* color space.
*/
private BufferedImage baseImg;
/**
* This is the Graphics2D
environment of the
* Layer
's image, which is really used to do all the
* graphics operations. It is replaced everytime the {@link #baseImg} itself
* is replaced.
*/
private Graphics2D g2;
/**
* Left edge of the layer as defined by the constructor or set later by the
* setter method. {@link #setX}.
NOTE: Do not
* change this value manually as results will be unpredictable.
*
*/
private int x;
/**
* Top edge of the layer as defined by the constructor or set later by
* {@link #setY}. NOTE: Do not change this value
* manually as results will be unpredictable.
*/
private int y;
/**
* Width of the layer as defined by the constructor or set later by
* {@link #resize(int, int)}. NOTE: Do not change
* this value manually as results will be unpredictable.
*/
private int width;
/**
* Left edge of the layer as defined by the constructor or set later by
* {@link #resize(int, int)}. NOTE: Do not change
* this value manually as results will be unpredictable.
*/
private int height;
/** The background of the image, need not be a Color
. */
private Paint backGround;
/**
* The background color of the image, the same as {@link #backGround} if
* bground
is a Color
else it is calculated
* based on the edges of the layer.
*/
private Color bgColor;
/**
* The opacity of the layer. This value is used when merging layers.
*
* @see #merge(Layer[]);
*/
private float opacity;
/**
* Same as opacity, the other way round ???
*/
private Color transparency;
/**
* MIME type of the object, as set by the user or when loading a picture
* from an external file.
*/
private String mimeType;
/** The luminance system in use - {@link #GAMMA22} by default */
private LuminanceSystem lumSys = GAMMA22;
/**
* The Composite
for layer merging.
*
* @see #merge(Layer[])
* @see #setLayerComposite
* @see #getLayerComposite
*/
private Composite layerComposite;
/**
* The index of the image in the multi-image source from which this layer
* has been loaded. This field defaults to zero and will be the requested
* image index as given to the {@link #Layer(InputStream, int)} constructor.
*/
private int imageIndex;
/**
* If this layer was created from a multi-image file (such as an animated)
* GIF the number of images in the file is stored here.
*/
private int numImages;
// ---------- construction
// --------------------------------------------------
/**
* Creates a Layer
with the indicated dimensions and
* background paint. The background paint may be any valid
* Paint
implementation but will usually be a simple
* Color
instance.
*
* The remaining properties of the layer are set to their default value. The
* origin is set to zero, the image type is GIF and the transparency color
* is not set.
*
* Note, that the background Paint
is painted into the newly
* created layer. If you later set the background Paint
to
* something else and resize the layer or merge it with other layer(s) some
* part of the layer will still have the old background color.
*
* @param width Width of the new layer. The minimum width of a layer is 1.
* @param height Height of the new layer. The minimum height of a layer is
* 1.
* @param bground The background paint of the new layer. This may be
* null
in which case the background is assumed to
* be in transparent white.
* @throws IllegalArgumentException if either the width or the height are
* specified lower than 1.
*/
public Layer(int width, int height, Paint bground) {
init(width, height, bground);
}
/**
* Creates a new Layer
instance by loading an image from the
* InputStream
.
*
* The width and height are set to the values of the image, while the other
* values are set to their respective default values as defined in the class
* comment above.
*
* This constructor is equivalent to calling the
* {@link #Layer(InputStream, int)} constructor with an index value or zero.
*
* @param input The InputStream to read the image data from
* @throws NullPointerException if the InputStream
is
* null
.
* @throws IOException if reading from the stream throws such an exception.
* @throws IIOException if decoding the image fails
*/
public Layer(InputStream input) throws IOException, IIOException {
this(input, 0, null);
}
/**
* Creates a new Layer
instance by loading an image from the
* InputStream
.
*
* The width and height are set to the values of the image, while the other
* values are set to their respective default values as defined in the class
* comment above.
*
* if max
is given, the loaded image
* will not be bigger than the given dimensions. eg: if the image is
* 1000x400 and the constraints are 500x500, the resulting layer will be
* 500x200.
*
* This constructor is equivalent to calling the
* {@link #Layer(InputStream, int, Dimension)} constructor with an index value or zero.
*
* @param input The InputStream to read the image data from
* @param max optional constraint for the maximal dimensions.
* @throws NullPointerException if the InputStream
is
* null
.
* @throws IOException if reading from the stream throws such an exception.
* @throws IIOException if decoding the image fails
*/
public Layer(InputStream input, Dimension max) throws IOException, IIOException {
this(input, 0, max);
}
/**
* Creates a new Layer
instance by loading the indexed image
* from the InputStream
.
*
* The width and height are set to the values of the image, while the other
* values are set to their respective default values as defined in the class
* comment above.
*
* @param input The InputStream to read the image data from
* @param idx The zero-based index of the image in the image data stream.
* The first image has index zero. This must not be a negative
* number.
* @throws NullPointerException if the InputStream
is
* null
.
* @throws IndexOutOfBoundsException If idx
is higher than
* the index of the last image in the image file.
* @throws IOException if reading from the stream throws such an exception.
* @throws IIOException if decoding the image fails
*/
public Layer(InputStream input, int idx) throws IOException, IIOException {
this(input, idx, null);
}
/**
* Creates a new Layer
instance by loading the indexed image
* from the InputStream
.
*
* The width and height are set to the values of the image, while the other
* values are set to their respective default values as defined in the class
* comment above. if max
is given, the loaded image
* will not be bigger than the given dimensions. eg: if the image is
* 1000x400 and the constraints are 500x500, the resulting layer will be
* 500x200.
*
* @param input The InputStream to read the image data from
* @param idx The zero-based index of the image in the image data stream.
* The first image has index zero. This must not be a negative
* number.
* @param max optional constraints for the maximal dimensions.
* @throws NullPointerException if the InputStream
is
* null
.
* @throws IndexOutOfBoundsException If idx
is higher than
* the index of the last image in the image file.
* @throws IOException if reading from the stream throws such an exception.
* @throws IIOException if decoding the image fails
*/
public Layer(InputStream input, int idx, Dimension max) throws IOException, IIOException {
this(input, idx, max, null);
}
/**
* Creates a new Layer
instance by loading the indexed image
* from the InputStream
.
*
* The width and height are set to the values of the image, while the other
* values are set to their respective default values as defined in the class
* comment above. if max
is given, the loaded image
* will not be bigger than the given dimensions. eg: if the image is
* 1000x400 and the constraints are 500x500, the resulting layer will be
* 500x200.
*
* @param input The InputStream to read the image data from
* @param idx The zero-based index of the image in the image data stream.
* The first image has index zero. This must not be a negative
* number.
* @param max optional constraints for the maximal dimensions.
* @param params an instance of ImageReadParam
.
* @throws NullPointerException if the InputStream
is
* null
.
* @throws IndexOutOfBoundsException If idx
is higher than
* the index of the last image in the image file.
* @throws IOException if reading from the stream throws such an exception.
* @throws IIOException if decoding the image fails
*/
public Layer(InputStream input, int idx, Dimension max,
ImageReadParam params) throws IOException, IIOException {
// closed and disposed in the finally block
ImageInputStream ios = null;
ImageReader reader = null;
boolean inputWrapped = false;
try {
/**
* If ImageIO has no matching reader, we will try then using Apache commons-imaging But ImageIO reads some
* bytes of the stream. That's why use mark/reset to get at the start of the data.
* Note: The max. reset length is deliberate but has proved to be rather
* stable by now - less would suffice it, too, I suspect
*/
if (!input.markSupported()) {
// Wrap the Stream using a mark-supporting stream
input = new BufferedInputStream(input, 1048576) {
// overwrite close to only remove the buffer but not
// close the wrapped InputStream
public void close() {
if (in == null) return;
in = null;
buf = null;
}
};
inputWrapped = true;
}
input.mark(1048576);
// Look for a reader for the input stream
ios = ImageIO.createImageInputStream(input);
Iterator readers = ImageIO.getImageReaders(ios);
while (readers.hasNext()) {
// Don't be picky. Take the first ImageReader and read the image.
// Note that the list is not sorted, so exactly which ImageReader is
// used might vary between calls or JVM instances (GRANITE-3651)
reader = (ImageReader) readers.next();
reader.setInput(ios, true);
if (params == null) {
params = reader.getDefaultReadParam();
}
IOException imageReadFailure = null;
if (max != null) {
max = new Dimension(max);
// get sub sampling values
ios.mark();
int samplefactor = 0;
try {
samplefactor = calculateSampleFactor(max, reader.getWidth(idx), reader.getHeight(idx));
} catch (IOException e) {
// may be caused by the reader unable to read
// the width and/or height. We ignore here for now
imageReadFailure = e;
}
try {
ios.reset();
} catch (IOException ie) {
// ignore any exception, most probably caused by a
// flushBefore call inside getWidth/getHeight which
// causes the mark to be lost
}
if (samplefactor > 1) {
params.setSourceSubsampling(samplefactor, samplefactor, 0, 0);
}
}
// unless previously failed for sample factor reading
BufferedImage fromInput = null;
if (imageReadFailure == null) {
try {
ios.mark();
fromInput = reader.read(idx, params);
} catch (IOException e) {
log.warn("Reading image with {} failed: {}. trying next reader.", reader, e.toString());
imageReadFailure = e;
} catch (IllegalArgumentException e) {
log.warn("Reading image with {} failed: {}. trying next reader.", reader, e.toString());
imageReadFailure = new IOException("Error reading image");
imageReadFailure.initCause(e);
}
}
if (imageReadFailure != null) {
// try next reader, if any
if (readers.hasNext()) {
reader.dispose();
ios.reset();
continue;
}
// all ImageIO readers failed, next try commons-imaging
try {
ios.reset();
input.reset();
fromInput = Imaging.getBufferedImage(input);
log.info("Reading image with apache-imaging was successful.");
} catch (ImageReadException e) {
log.warn("reading image with commons-imaging failed: {}", e.toString());
// no more readers, rethrow this
throw imageReadFailure;
}
}
if (fromInput == null) {
// resilience: we don't expect this situation
throw new IllegalStateException("Unexpected missing image");
}
imageIndex = idx;
ColorModel cm = fromInput.getColorModel();
if (cm instanceof IndexColorModel) {
// convert the color model to RGBA and set image
IndexColorModel icm = (IndexColorModel) cm;
if (max != null) {
init(max.width, max.height, TRANSPARENT_IMAGE_BACKGROUND);
BufferedImage bm = icm.convertToIntDiscrete(fromInput.getRaster(), true);
ResizeOp.doFilter_progressive(bm, baseImg);
bm.flush();
} else {
// initialize with a small dummy image
init(1, 1, TRANSPARENT_IMAGE_BACKGROUND);
setImage(icm.convertToIntDiscrete(fromInput.getRaster(), true));
}
} else {
/**
* We copy the image read into a new buffered image as the
* image reader may not return the correct image type I
* want.
*/
// initialize the image and draw the image read
if (max != null) {
init(max.width, max.height, TRANSPARENT_IMAGE_BACKGROUND);
ResizeOp.doFilter_progressive(fromInput, baseImg);
} else {
init(1, 1, TRANSPARENT_IMAGE_BACKGROUND);
setImage(fromInput);
fromInput = null;
}
}
/**
* For gif-only we read the metadata. This is not really
* portable, as we directly access sun's GIF metadata
* implementation
*/
mimeType = reader.getOriginatingProvider().getMIMETypes()[0];
if (mimeType.toLowerCase().endsWith("gif")) {
// get the meta data from the GIF image
ImageSupport.getGIFMetaData(this, reader);
}
// Release fromInput
if (fromInput != null) {
fromInput.flush();
}
// try to get the number of images in the file ...
for (numImages = imageIndex + 1;; numImages++) {
try {
reader.read(numImages).flush();
} catch (IndexOutOfBoundsException ioo) {
// got the number
break;
} catch (Throwable t) {
// any other, don't care for now
break;
}
}
// done
return;
}
// ImageIO has no decoder for the image, fail
throw new IIOException("No decoder available to load the image");
} catch (OutOfMemoryError oome) {
// may be the case if caching the image failed
throw new IIOException("Not enough memory to load the image");
} finally {
// dispose reader
if (reader != null) {
reader.dispose();
}
// close ios
try {
if (ios != null) {
ios.close();
}
} catch (IOException ignore) {
}
// close the wrapping Buffer Stream
if (inputWrapped) {
try {
input.close();
} catch (IOException ignore) {
}
}
}
}
protected static int calculateSampleFactor(Dimension max, int w, int h) {
int tw = w;
int th = h;
if (max.width > 0 && max.width < tw) {
th = h * max.width / w;
tw = max.width;
}
if (max.height > 0 && max.height < th) {
tw = w * max.height / h;
th = max.height;
}
// adjust dimensions
if (tw != 0) {
max.width = tw;
}
if (th != 0) {
max.height = th;
}
if (tw == 0 || th == 0) {
return 0;
}
// only do subsampling for large images
if (tw > th && tw < MAX_SUBSAMPLING_SIZE) {
tw = MAX_SUBSAMPLING_SIZE;
}
if (th > tw && th < MAX_SUBSAMPLING_SIZE) {
th = MAX_SUBSAMPLING_SIZE;
}
return Math.min(w/tw, h/th);
}
/**
* Creates a new Layer
by copying the image of the source
* layer and setting all properties to the exact same value they are set in
* the source layer.
*
* @param src The layer to copy
* @throws NullPointerException if the source layer is null
.
*/
public Layer(Layer src) {
// Create a new Layer based on the source size and background
init(src.width, src.height, src.backGround);
// Copy over the rest of the attributes
this.x = src.x;
this.y = src.y;
this.opacity = src.opacity;
this.transparency = src.transparency;
this.mimeType = src.mimeType;
this.layerComposite = src.layerComposite;
// copy image index information
this.imageIndex = src.imageIndex;
this.numImages = src.numImages;
// Now copy the image
g2.drawRenderedImage(src.getImage(), IDENTITY_XFORM);
}
/**
* Creates a new Layer
by wrapping the
* BufferedImage
with the Layer
properties.
* Note that this really is a wrapping constructor and not a copy
* constructor. That is if you keep drawing into the original image, you get
* a mixed result.
*
* @param image The BufferedImage
to wrap as a
* Layer
.
* @throws NullPointerException if the image is null
.
*/
public Layer(BufferedImage image) {
if (image == null) {
throw new NullPointerException("image");
}
setImage(image, false);
// final initialization of the fields
this.x = DEFAULT_IMAGE_ORIGINX;
this.y = DEFAULT_IMAGE_ORIGINY;
// this.height -- set in initBaseImage()
// this.width -- set in initBaseImage()
this.bgColor = TRANSPARENT_IMAGE_BACKGROUND; // DEFAULT_IMAGE_BACKGROUND;
this.backGround = this.bgColor;
this.opacity = DEFAULT_IMAGE_OPACITY;
this.transparency = null;
this.mimeType = DEFAULT_MIME_TYPE;
this.layerComposite = DEFAULT_LAYER_COMPOSITE;
}
/**
* Write the image to the given OutputStream
using the
* desired MIME type. If the MIME type is empty or null, we use the MIME
* type of the layer.
*
* The quality parameter is used to define the compression level of JPEG
* image creation, if JPEG output is desired as per the MIME type. The
* quality value must be in the range 0.0 .. 1.0 inclusive. Any value
* outside this range results in the default compression factor
* 0.82 being used.
*
* For GIF images the colors will be reduced according to the quality
* argument. If the argument is missing, at most 256 colors will be in the
* image or as much as need be.
*
* Note that specifying the MIME type for the image type is simply used to
* decide on the output format to use for writing. Especially the method is
* not able set any Content-Type headers whatsoever.
*
* The OutputStream is neither flushed nor closed at the end. It is the sole
* responsibilty of the client of this method to do so.
*
* @param mimeType MIME type to use for writing. If empty or null the MIME
* type of the image will be used. As a last fall back the
* default MIME type as per {@link #DEFAULT_MIME_TYPE} is used.
* @param quality Defines the JPEG compression quality (0.0 .. 1.0) or the
* numbers of colors to use for the GIF image.
* @param outStream OutputStream
to use to write the layer.
* @return true if the layer could be written, else false is returned.
* @throws IllegalArgumentException if a mimeType is specified either as a
* parameter or as the layer's image type, which is not
* supported for writing by the ImageIO system.
* @throws NullPointerException if the OutputStream
is
* null
.
* @throws IIOException we get from the ImageIO Library we use.
* @throws IOException we get from writing to the OutputStream
.
*/
public boolean write(String mimeType, double quality, OutputStream outStream)
throws IIOException, IOException {
// Check the MIME type
if ((mimeType == null) || (mimeType.length() == 0)) {
mimeType = this.mimeType;
}
// fall back to default MIME type, if not set for the layer
if ((mimeType == null) || (mimeType.length() == 0)) {
mimeType = DEFAULT_MIME_TYPE;
}
// get the format out of the mimetype
String format = mimeType.substring(mimeType.indexOf('/') + 1);
// closed and disposed off in finally block
ImageWriter writer = null;
ImageOutputStream ios = null;
try {
writer = ImageSupport.getImageWriter(format);
if (writer != null) {
// Attach the outStream to the ImageIO
ios = ImageIO.createImageOutputStream(outStream);
writer.setOutput(ios);
// The IIOImage to draw
IIOMetadata streamMetadata = null;
IIOMetadata imageMetadata = null;
ImageWriteParam iwp = null;
int targetImageType = getImage().getType();
if ("gif".equalsIgnoreCase(format)) {
// this will pass the transparency color to the gif writer, if set
IIOMetadata gifMeta[] = ImageSupport.createGIFMetadata(
this, writer, (int) quality);
// base image might have been replaced by a color-reduced one, adapt type flag
targetImageType = getImage().getType();
if (gifMeta[0] != null) {
streamMetadata = gifMeta[0];
}
if (gifMeta[1] != null) {
imageMetadata = gifMeta[1];
}
} else if ("jpg".equalsIgnoreCase(format)
|| "jpeg".equalsIgnoreCase(format)) {
// check quality as image quality 0..1
if (quality < 0 || quality > 1) {
quality = 0.82;
}
// write parameters
iwp = new JPEGImageWriteParam(null);
iwp.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
iwp.setCompressionQuality((float) quality);
// no alpha channel for jpeg, specific type needed for writer
targetImageType = BufferedImage.TYPE_INT_RGB;
} else {
// force the transparent color to be transparent
if (transparency != null) {
long trans = transparency.getRGB();
replaceColor(trans, trans & 0x00ffffff, false);
}
}
/**
* This following little hack finds a BufferedImage type which
* the writer is able to write to. Unfortunately the ImageIO
* does not provide an API to create a BufferedImage which is
* writable from another BufferedImage which is not writable
* So we loop over all known BufferedImage types - which are
* assumed to start with TYPE_INT_RGB (==1) incrementing
* consecutively to TYPE_BYTE_INDEXED (==13).
*/
ImageTypeSpecifier its = ImageTypeSpecifier.createFromRenderedImage(getImage());
if (!writer.getOriginatingProvider().canEncodeImage(its)) {
for (int bit = BufferedImage.TYPE_INT_RGB; bit <= BufferedImage.TYPE_BYTE_INDEXED; bit++) {
its = ImageTypeSpecifier.createFromBufferedImageType(bit);
if (writer.getOriginatingProvider().canEncodeImage(its)) {
targetImageType = bit;
break;
}
}
}
// define the writable image
BufferedImage writableImage;
if (targetImageType != getImage().getType()) {
boolean sourceHasAlpha = getImage().getColorModel().hasAlpha();
boolean targetSupportsAlpha = ImageTypeSpecifier.createFromBufferedImageType(targetImageType).getColorModel().hasAlpha();
// paint the original image into the new image
writableImage = new BufferedImage(width, height, targetImageType);
Graphics2D g2d = writableImage.createGraphics();
// mostly for writing JPEGs
// if the target image (file format) does not support an alpha channel,
// but the source image has one, we need to fill it with the background color
if (sourceHasAlpha && !targetSupportsAlpha) {
g2d.drawImage(getImage(), 0, 0, getBackgroundColor(), null);
} else {
g2d.drawRenderedImage(getImage(), IDENTITY_XFORM);
}
} else {
writableImage = getImage();
}
// write the writable image
if (writer.getOriginatingProvider().canEncodeImage(writableImage)) {
IIOImage image = new IIOImage(writableImage, null, imageMetadata);
writer.write(streamMetadata, image, iwp);
}
} else {
// If getImageWriteByMIMEType returns an empty iterator, i.e. no
// writer could be found by the ImageIO we cannot write and fail
return false;
}
} catch (IllegalArgumentException iae) {
// This should be thrown by getImageWritersByMIMEType() if
// argument is null -> should not happen !
// log
return false;
} catch (IOException ioe) {
// log or handle
return false;
} catch (OutOfMemoryError oome) {
// may be the case if caching the image failed
throw new IIOException("Not enough memory to store the image");
} finally {
// dispose of writer
if (writer != null) {
writer.dispose();
}
// close output stream
if (ios != null) {
try {
ios.close();
} catch (IOException ignore) {
}
}
}
return true;
}
/**
* Releases as much resources as possible. This method is called when the
* layer is not intended to be used any more.
*
* Note that using the layer object again after calling this method results
* in unexpected behaviour and at least throwing of
* NullPointerException
s.
*/
public void dispose() {
/**
* Explicitly set all references to null
to support the
* GC finding unused objects.
*/
// only flush graphics if set
if (g2 != null) {
g2.dispose();
g2 = null;
}
// only flush base image if set
if (baseImg != null) {
baseImg.flush();
baseImg = null;
}
backGround = null;
bgColor = null;
lumSys = null;
mimeType = null;
transparency = null;
}
// ---------- layer copying
/**
* Merges a layer onto the current layer whereby obeying the layer's opacity
* level. The size of the current Layer is adapted to the compound size of
* both layers merged.
*
* @param layer the Layer
s to merge onto this one. If
* null
this layer is not changed.
*/
public void merge(Layer layer) {
if (layer != null) {
merge(new Layer[] { layer });
}
}
/**
* Merges a number of layers onto the current layer whereby obeying each
* layers opacity level. The layers are merged in the sequence the are
* listed in the array. The size of the current Layer is adapted to the
* compound size of all layers merged.
*
* Each layer is painted over the already painted layers according to the
* {@link #getOpacity opacity} and {@link #getLayerComposite composite} set
* on the layer to be painted. If the {@link #getLayerComposite} is not a
* AlphaComposite
, the {@link #getOpacity opacity} is
* ignored.
*
* This method is conservative in that it is a null operation if the layers
* is null
or empty.
*
* @param layers the list of Layer
s to merge onto this one.
*/
public void merge(Layer[] layers) {
// Guard against empty mergers
if (layers == null || layers.length == 0) {
// log
return;
}
int newX = x;
int newY = y;
int newR = x + width /* - 1 */;
int newB = y + height /* -1 */;
// Get max dimensions of the layers
for (int i = 0; i < layers.length; i++) {
Layer l = layers[i];
int r = l.x + l.width;
int b = l.y + l.height;
if (newX > l.x) newX = l.x;
if (newY > l.y) newY = l.y;
if (newR < r) newR = r;
if (newB < b) newB = b;
}
// Calculate new width
int newW = newR - newX;
int newH = newB - newY;
// Only create new image, if needed
if (newX != x || newY != y || newW != width || newH != height) {
// store old image and settings and release old g2
BufferedImage oldImage = getImage();
Graphics2D oldG2 = getG2();
Composite oldComposite = oldG2.getComposite();
Paint oldPaint = g2.getPaint();
Stroke oldStroke = g2.getStroke();
AffineTransform oldAffineTransform = g2.getTransform();
// create new image, initialize and apply settings
BufferedImage newImage = new BufferedImage(newW, newH, IMAGE_TYPE);
Graphics2D newG2 = newImage.createGraphics();
newG2.setPaint(backGround);
newG2.fillRect(0, 0, newW, newH);
// Copy over old image
newG2.drawRenderedImage(oldImage, AffineTransform.getTranslateInstance(x - newX, y - newY));
// "install" new image
newG2.dispose();
setImage(newImage);
getG2().setComposite(oldComposite);
getG2().setPaint(oldPaint);
getG2().setStroke(oldStroke);
getG2().setTransform(oldAffineTransform);
// adapt new origin
x = newX;
y = newY;
}
// store for later reset
Composite oldComposite = g2.getComposite();
// Start painting the old layers to the new one
// using an AlphaComposite(SRC_OVER, layer.opacity)
for (int i = 0; i < layers.length; i++) {
// get opacity and layer's composite
float op = layers[i].opacity;
Composite composite = layers[i].layerComposite;
// if not opaque and composite is an AlphaComposite get
// partially transparent composite
if (op < 0.99999 && composite instanceof AlphaComposite) {
int acRule = ((AlphaComposite) composite).getRule();
composite = AlphaComposite.getInstance(acRule, op);
}
g2.setComposite(composite);
g2.drawRenderedImage(layers[i].getImage(),
AffineTransform.getTranslateInstance(layers[i].x - newX,
layers[i].y - newY));
}
// reset to old composite
g2.setComposite(oldComposite);
}
/**
* Copy a subimage from the given source layer to this layer. The copied
* image is hooked in the top left corner of the Layer
.
*
* @param src the source Layer
to copy from
* @param dw the width of reactangle to copy
* @param dh the height of the rectangle to copy.
* @see #blit(Layer, int, int, int, int, int, int)
*/
public void blit(Layer src, int dw, int dh) {
blit(src, 0, 0, dw, dh, 0, 0);
}
/**
* Copy a subimage from the given source layer to this layer. The source
* rectangle is fully specified by its origin, width and height while the
* destination position of the top left corner is also given.
*
* @param src the source Layer
to copy from
* @param dx the left edge of the destination area in this layer
* @param dy the top edge of the destination area in this layer
* @param dw the width of reactangle to copy
* @param dh the height of the rectangle to copy.
* @param sx the left edge of the source area in the source layer
* @param sy the top edge of the source area in the source layer
*/
public void blit(Layer src, int dx, int dy, int dw, int dh, int sx, int sy) {
BufferedImage srcImage = src.getImage().getSubimage(sx, sy, dw, dh);
g2.drawRenderedImage(srcImage, AffineTransform.getTranslateInstance(dx,
dy));
}
/**
* Copy the color or alpha channel from the source the same or another color
* or alpha channel in this layer. Nothing is done, if the method would do
* an identity copy, that is l.copyChannel(null, c, c)
will
* do nothing.
*
* @param src the source layer for the channel copy, if null
* this is used as the src
.
* @param fromChannel channel to copy from the the source layer. If
* negative, the alpha channel will be copied
* @param toChannel channel in this layer to copy the fromChannel into. If
* negative the same channel as fromChannel will be used.
* @throws IndexOutOfBoundsException if the channel number is higher than
* number of available channels in either the source or this
* layer.
*/
public void copyChannel(Layer src, int fromChannel, int toChannel) {
// Check parameters
if (src == null) src = this;
if (fromChannel < 0) fromChannel = ALPHA_CHANNEL_ID;
if (toChannel < 0) toChannel = fromChannel;
// return early if copying the same channel
if (src == this && fromChannel == toChannel) {
return;
}
int w = Math.min(width, src.width);
int h = Math.min(height, src.height);
if (fromChannel > src.getImageRGBA().getRaster().getNumBands()) {
throw new IndexOutOfBoundsException("fromChannel");
}
if (toChannel > getImageRGBA().getRaster().getNumBands()) {
throw new IndexOutOfBoundsException("toChannel");
}
getImageRGBA().getRaster().setSamples(
0,
0,
w,
h,
toChannel,
src.getImageRGBA().getRaster().getSamples(0, 0, w, h, fromChannel,
(int[]) null));
}
/**
* Colorizes the identical pixel of the source and destination layer with
* the indicated color. This can be used to produce a transparent text over
* a background image.
*
* @param src the source layer for the masking operation or
* null
this.
* @param col the color to set for identical pixels. If null
* opaque black is used.
*/
public void colorMask(Layer src, Color col) {
// Check parameters
if (src == null) src = this;
if (col == null) col = Color.black;
int w = Math.min(width, src.width);
int h = Math.min(height, src.height);
int color = col.getRGB();
// Get the original color values
BufferedImage image = getImageRGBA();
int[] de = (int[]) image.getRaster().getDataElements(0, 0, w, h, null);
int[] se = (int[]) src.getImageRGBA().getRaster().getDataElements(0, 0, w, h,
null);
// replace color values
for (int i = 0; i < se.length; i++) {
if (se[i] == de[i]) de[i] = color;
}
// set new color values
image.getRaster().setDataElements(0, 0, w, h, de);
}
// ---------- layer modification
// --------------------------------------------
/**
* Adapt the alpha channel of the image so that each color value has its
* value multiplied with the alpha value. For each pixel not set the
* indicated color is set.
*
* The DST_OVER composite paints the background only as much as the alpha
* channel of the layer pixels allows also respecting the alpha value of the
* background color !!
*
* @param color The color to use for unlit pixels. Set to a negative value
* to use the background color of the layer.
*/
public void flatten(Color color) {
if (color == null) {
if (backGround instanceof Color) {
color = (Color) backGround;
} else {
color = getBackgroundColor();
}
}
// save old settings
Paint op = g2.getPaint();
Composite oc = g2.getComposite();
// new settings and paint
g2.setPaint(color);
g2.setComposite(AlphaComposite.DstOver);
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_OFF);
g2.fillRect(0, 0, width, height);
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
// restore old settings
g2.setComposite(oc);
g2.setPaint(op);
}
/**
* Rotates the layer by the given angle in degrees in clockwise direction.
*
* @param degrees the angle to rotate the layer by. Usually this angle
* should be in the range 0-360�.
*/
public void rotate(double degrees) {
double theta = Math.toRadians(degrees);
double cos = Math.cos(theta);
double sin = Math.sin(theta);
int xmin, xmax, ymin, ymax;
int q = (int) (degrees / 90); // [ 0 .. 3 ];
if ((q & 1) == 1) {
// quadrant 1 or 3;
double y2 = width * sin + 0 * cos;
double y4 = 0 * sin + height * cos;
double x1 = 0;
double x3 = width * cos - height * sin;
if (q == 1) {
ymin = (int) Math.floor(y4);
ymax = (int) Math.ceil(y2);
xmin = (int) Math.floor(x3);
xmax = (int) Math.ceil(x1);
} else {
ymin = (int) Math.floor(y2);
ymax = (int) Math.ceil(y4);
xmin = (int) Math.floor(x1);
xmax = (int) Math.ceil(x3);
}
} else {
// quadramt 0 or 4;
double y1 = 0;
double y3 = width * sin + height * cos;
double x2 = width * cos - 0 * sin;
double x4 = 0 * cos - height * sin;
if (q == 0) {
ymin = (int) Math.floor(y1);
ymax = (int) Math.ceil(y3);
xmin = (int) Math.floor(x4);
xmax = (int) Math.ceil(x2);
} else {
ymin = (int) Math.floor(y3);
ymax = (int) Math.ceil(y1);
xmin = (int) Math.floor(x2);
xmax = (int) Math.ceil(x4);
}
}
// Dimension is distance between min and max
width = xmax - xmin;
height = ymax - ymin;
// Translation is only relevant if min is negative
xmin = (xmin < 0) ? -xmin : 0;
ymin = (ymin < 0) ? -ymin : 0;
AffineTransform rot = new AffineTransform();
rot.translate(xmin, ymin);
rot.rotate(theta);
BufferedImage newImage = new BufferedImage(width, height, IMAGE_TYPE);
transformImage(newImage, rot);
}
/**
* Flips the layer horizontally.
*/
public void flipHorizontally() {
AffineTransform scale = new AffineTransform();
scale.scale(-1.0, 1.0);
scale.translate(-width, 0.0);
BufferedImage newImage = new BufferedImage(width, height, IMAGE_TYPE);
transformImage(newImage, scale);
}
/**
* Flips the layer vertically.
*/
public void flipVertically() {
AffineTransform scale = new AffineTransform();
scale.scale(1.0, -1.0);
scale.translate(0.0, -height);
BufferedImage newImage = new BufferedImage(width, height, IMAGE_TYPE);
transformImage(newImage, scale);
}
/**
* Resize the image scaling it by the scaling factor indicated by the new
* width and height parameters. If the aspect ration of the existing layer
* should be kept, the new width and height values should be set
* accordingly.
*
* @param width the new width of the layer. Set to 0 or a negative value to
* keep the current width.
* @param height the new height of the layer. Set to 0 or a negative value
* to keep the current height.
*/
public void resize(int width, int height) {
resize(width, height, false);
}
/**
* Resize the image scaling it by the scaling factor indicated by the new
* width and height parameters. If the aspect ration of the existing layer
* should be kept, the new width and height values should be set
* accordingly.
*
* @param width the new width of the layer. Set to 0 or a negative value to
* keep the current width.
* @param height the new height of the layer. Set to 0 or a negative value
* to keep the current height.
* @param fast if set to true
a faster resizing algorithm is
* used but with poorer detail.
*/
public void resize(int width, int height, boolean fast) {
if (width <= 0) width = this.width;
if (height <= 0) height = this.height;
if (width == this.width && height == this.height) {
return;
}
double sx = (double) width / (double) this.width;
double sy = (double) height / (double) this.height;
// lets do the resize
ResizeOp op = new ResizeOp(sx, sy, g2.getRenderingHints());
op.setFast(fast);
setImage(op.filter(getImageRGBA(), null));
}
/**
* Get the subimage indicated by the rectangle from the layer. The subimage
* is simply cut out the bigger one.
*
* @param rect the rectangle definiing the part of the image to cut out
*/
public void crop(Rectangle2D rect) {
rect = checkRect(rect);
BufferedImage newImage = getImage().getSubimage((int) rect.getX(),
(int) rect.getY(), (int) rect.getWidth(), (int) rect.getHeight());
// Won't be set by transformImage()
// x = (int) rect.getX(); -- 0 according to rgba2.c/rgbaCropLayer()
// y = (int) rect.getY(); -- 0 according to rgba2.c/rgbaCropLayer()
transformImage(newImage, null);
}
/**
* Embosses the layer using the bump (if specified) and the light direction
* specified by aizmut and elevation.
NOTE: This
* implementation is a copy of the existing rgbaLayer routine. It may
* therefore not be very fast. But it works the same as the corresponding
* ECMA routine ;-)
*
* @param bump the bump layer to emboss this layer with. If null this layer
* itself will also be used as the bump.
* @param azimut the light direction azimut. Specify a negative value to get
* the default value of 30.
* @param elevation the light direction elevation. Specify a negative value
* to get the default value of 30.
* @param filtersize the filtersize for the embossing. Specify a negative
* value to get the default value of 3.
*/
public void emboss(Layer bump, int azimut, int elevation, int filtersize) {
// Check parameters
if (bump == null) bump = this;
if (azimut < 0) azimut = 30;
if (elevation < 0) elevation = 30;
if (filtersize < 0) filtersize = 3;
// bug #8053 - backwards compatiblity light source seems to be
// slightly on another position
azimut += 180;
EmbossOp op = new EmbossOp(bump.getImageRGBA(), azimut, elevation, filtersize);
BufferedImage newImage = op.filter(getImageRGBA(), null);
setImage(newImage);
}
/**
* Distorts the layer along a 4-edged shape. The transformation of the
* rectangle may lead to a rectangle of a different size. Depending on the
* crop
parameter, the size of the layer is adapted to the
* new size or not. If crop==true
, the layer's size is
* either enlarged or made smaller depending on the bounding box of the
* transformed layer.
*
* This method internally uses the {@link DistortOp} class to calculate the
* transformed layer. Therefore the same warning about memory usage applies
* to this method as does to the {@link DistortOp} class.
*
* @param x1 x-coordinate of top left corner of transformed rectangle
* @param y1 y-coordinate of top left corner of transformed rectangle
* @param x2 x-coordinate of top right corner of transformed rectangle
* @param y2 y-coordinate of top right corner of transformed rectangle
* @param x3 x-coordinate of bottom left corner of transformed rectangle
* @param y3 y-coordinate of bottom left corner of transformed rectangle
* @param x4 x-coordinate of bottom right corner of transformed rectangle
* @param y4 y-coordinate of bottom right corner of transformed rectangle
* @param crop true
of the layer should be adapted to the
* bounding of the transformed rectangle.
*/
public void xForm(int x1, int y1, int x2, int y2, int x3, int y3, int x4,
int y4, boolean crop) {
// scale to relative factors !!!
float fx1 = (float) x1 / width;
float fx2 = (float) x2 / width;
float fx3 = (float) x3 / width;
float fx4 = (float) x4 / width;
float fy1 = (float) y1 / height;
float fy2 = (float) y2 / height;
float fy3 = (float) y3 / height;
float fy4 = (float) y4 / height;
float[][] coords = new float[][] { { fx1, fy1 }, { fx2, fy2 },
{ fx3, fy3 }, { fx4, fy4 } };
BufferedImageOp bop = new DistortOp(coords, crop, getBackgroundColor());
BufferedImage newImg = bop.filter(getImageRGBA(), null);
setImage(newImg);
}
/**
* Converts the color image into a grayscale image
*/
public void grayscale() {
/**
* How this works : (1) make sure the alpha value is applied to the
* pixel such that the pixel values resemble the real intensity (2)
* replace each pixel component value with a value which shows the color
* intensity of said pixel (3) the alpha channel value is not changed
* during this operation
*/
// gray scale image
float[][] bwBopEl = { { lumSys.r(), lumSys.g(), lumSys.b(), 0, 0 },
{ lumSys.r(), lumSys.g(), lumSys.b(), 0, 0 },
{ lumSys.r(), lumSys.g(), lumSys.b(), 0, 0 }, { 0, 0, 0, 1, 0 } };
// make sure alpha is premultiplied
setImage(ImageSupport.coerceData(getImageRGBA(), true));
// could optimize by caching the band combine op ...
WritableRaster raster = getImageRGBA().getRaster();
BandCombineOp bco = new BandCombineOp(bwBopEl, null);
bco.filter(raster, raster);
}
/**
* Colorizes the layer, i.e. map the brightness of the image onto the
* gradient from darkcolor to brightcolor.
*
* @param darkcolor the dark start color for the colorization. If negative
* 0xff000000 (black) will be used.
* @param brightcolor the bright end color for the colorization. If negative
* 0xffffffff (white) will be used.
*/
public void colorize(Color darkcolor, Color brightcolor) {
// grayscale needed by multitone op
grayscale();
// define the color curves for the colorization
ColorCurve[] curves = new ColorCurve[2];
curves[0] = new ColorCurve(darkcolor, new float[] { 0, 1 });
curves[1] = new ColorCurve(brightcolor, new float[] { 1, 0 });
// apply the multi tone operation using these curves
MultitoneOp mo = new MultitoneOp(curves, null);
BufferedImage image = getImageRGBA();
mo.filter(image, image);
}
/**
* Monotonize the image with a base color other than black. This method is
* equivalent to calling multitone(new Color[]{ color })
.
*
* @param color Color to use as the base for the monotonized image
* @throws NullPointerException if the color value is null
.
*/
public void monotone(Color color) {
if (color == null) {
throw new NullPointerException("color");
}
multitone(new Color[] { color });
}
/**
* Multitone an image. The luminance scaled image is applied the listed
* colors in order to get the multitoned image result.
*
* Note that {@link #monotone(Color)} is the single color special case of
* multitone.
*
* This method does not yet work as expected
*
* @param colors The colors to apply.
* @throws NullPointerException if the colors array or any of the elements
* in the array is null
.
* @throws IllegalArgumentException if the colors array is empty
*/
public void multitone(Color[] colors) {
if (colors == null) {
throw new NullPointerException("colors");
}
if (colors.length == 0) {
throw new IllegalArgumentException("empty colors");
}
// the number of colors in the list
int numcols = colors.length;
// check color entries
for (int i = 0; i < numcols; i++) {
if (colors[i] == null) {
throw new NullPointerException("colors[" + i + "]");
}
}
// grayscale needed by multitone op
grayscale();
// apply the multitone operation with the colors
MultitoneOp mo = new MultitoneOp(colors, null);
BufferedImage image = getImageRGBA();
mo.filter(image, image);
}
/**
* Colorize the image according to the color curves. This operation is
* similar to the Photohop Duplex operation where you define a
* number of colors and optional tone curves.
*
* @param colorCurves The color curves to use in the {@link MultitoneOp}
* filter.
* @see MultitoneOp
* @see ColorCurve
*/
public void multitone(ColorCurve[] colorCurves) {
// grayscale needed by multitone op
grayscale();
// apply the multitone operation with the curves
MultitoneOp mo = new MultitoneOp(colorCurves, null);
BufferedImage image = getImageRGBA();
mo.filter(image, image);
}
/**
* Constant defining the maximal size of the kernel for the
* ConvolveOp
of the
* {@link #blur(double, double, int, double, double)} method.
*/
private static final int RGBA_BLUR_KERNEL_MAX = 256;
/**
* Half the maximal size of the kernel.
*/
private static final int RGBA_BLUR_KERNEL_HALF = RGBA_BLUR_KERNEL_MAX / 2;
/**
* Blurs the picture in the layer using the values given. Optionally only
* one of the color channels is blurred while all the others remain
* untouched. This method is simply a reimplementation of the ECMA method
* using the ConvolveOp
class of the Java 2D API. Maybe we
* should specify the matrix better ?
*
* @param radius (default:1)
* @param scale (default:1)
* @param flags (default:all channels) - not used at the moment
* @param gran (default:1)
* @param maxdata (default:255)
*/
public void blur(double radius, double scale, int flags, double gran,
double maxdata) {
// Check parameters
if (radius < 0.0) radius = 1.0;
if (scale < 0.0) scale = 1.0;
if (flags < 0) flags = IMAGE_TYPE;
if (gran < 0.0) gran = 1.0;
if (maxdata < 0) maxdata = 255.0;
double kField[] = new double[RGBA_BLUR_KERNEL_MAX];
double max = 0.0f;
double delta = gran / (2.0 * maxdata);
int kernelEdge = 0;
float kField2[];
for (int j = 0; j < RGBA_BLUR_KERNEL_MAX; j++) {
double tmp = (j - RGBA_BLUR_KERNEL_HALF) / radius;
kField[j] = Math.exp(-tmp * tmp / 2);
max += kField[j];
}
int kernelsize = RGBA_BLUR_KERNEL_MAX - 1;
double tmp = 2 * (kField[kernelsize] / max);
while ((tmp < delta) && (kernelsize > RGBA_BLUR_KERNEL_HALF)) {
tmp = tmp + 2 * kField[kernelsize] / max;
kField[kernelsize] = 0.0;
kField[RGBA_BLUR_KERNEL_MAX - kernelsize] = 0.0;
kernelsize--;
}
/* start defining the matrix for ConvolveOp */
kernelEdge = 2 * kernelsize - RGBA_BLUR_KERNEL_MAX;
if (kernelEdge == 0) return;
kField2 = new float[kernelEdge * kernelEdge];
/* set the matrix values */
int kfoff = RGBA_BLUR_KERNEL_MAX - kernelsize;
for (int x = 0; x < kernelEdge; x++) {
for (int y = 0; y < kernelEdge; y++) {
int off = kernelEdge * y + x;
kField2[off] = (float) (kField[x + kfoff] + kField[y + kfoff]);
max += kField2[off];
}
}
/* average weights so that sum(kernel)==1 */
max /= scale;
for (int i = 0; i < kField2.length; i++) {
kField2[i] /= max;
}
Kernel kernel = new Kernel(kernelEdge, kernelEdge, kField2);
ConvolveOp blur = new ConvolveOp(kernel, ConvolveOp.EDGE_NO_OP,
g2.getRenderingHints());
BufferedImage image = getImageRGBA();
setImage(blur.filter(image, null));
}
/**
* Sharpens the image by applying a convolution operation which involves
* ehancing the center of a kernel. Each element of the convolution matirx
* is set to -1, while the center - which is the source pixel - is set to
* the negative sum of all the other elements plus the given percentage
* amount of that sum. That is, the pixel is enhanced relativ to the
* neighbours.
*
* @param amount Center enhancement in percent in the range 0 .. 1.0
* @param radius Size of the convolution matrix. The size is rounded to and
* integral matrix edge according to
* edge = int(2 * (radius + 1))
. That is the
* minimum matrix size is guaranteed to be 1, which has no effect
* whatsoever. Allowed range for this value is 0.5 .. 10.0
*/
public void sharpen(float amount, float radius) {
// Check values
if (amount <= 0 || radius < 0.5) return; // noop
if (amount > 1) amount = 1;
if (radius > 10) radius = 10;
// radius are pixels on each side plus center pixel gives edge
int edge = 2 * (int) (radius + 0.5) + 1;
// Prepare matrix, setting everything to -1;
float[] matrix = new float[edge * edge];
for (int i = 0; i < matrix.length; i++)
matrix[i] = -1;
// Calculate the center piece
matrix[(edge + 1) * (edge / 2)] = matrix.length - 1 + amount;
// Create the operation
Kernel kernel = new Kernel(edge, edge, matrix);
ConvolveOp sharpen = new ConvolveOp(kernel);
// Do it
setImage(sharpen.filter(getImage(), null));
}
/**
* Recombines the channels of the layer. According to matrix4obj and
* vector4obj the channels (alpha, red, green, blue) of the layer are
* recombined and possibly scaled.
*
* @param matrix 4x4 matrix to multiply to the vector of channel values of
* each pixel
* @param vector array of 4 elements to add to each resulting value got from
* the multiplication.
* @param crop default true
*/
public void xFormColors(double[][] matrix, double[] vector, boolean crop) {
// copy the matrix
float[][] bopEl = new float[4][5];
for (int i = 0; i < 4 && i < matrix.length; i++) {
for (int j = 0; j < 4 && j < matrix[i].length; j++) {
bopEl[i][j] = (float) matrix[i][j];
}
}
// copy the additive vector
for (int i = 0; i < 4 && i < vector.length; i++) {
bopEl[i][4] = (float) vector[i];
}
// apply the operation inplace
BufferedImage image = getImageRGBA();
new BandCombineOp(bopEl, null).filter(image.getRaster(), image.getRaster());
}
/**
* Sets all pixels having color1 to color2. The alpha channel value is only
* touched if ignoreAlpha is not set.
*
* @param color1 Color identifying pixels to be modified
* @param color2 New color of those pixels
* @param ignoreAlpha Set to true to not touch the alpha channel of the
* pixels.
*/
public void replaceColor(long color1, long color2, boolean ignoreAlpha) {
BufferedImage image = getImageRGBA();
int[] rgbArray = image.getRGB(0, 0, width, height, null, 0, width);
int len = rgbArray.length;
int c1 = (int) color1;
int c2 = (int) color2;
if (ignoreAlpha) {
// ignore alpha channels of the colors
c1 &= 0x00ffffff;
c2 &= 0x00ffffff;
for (int i = 0; i < len; i++) {
if ((rgbArray[i] & 0x00ffffff) == c1) {
rgbArray[i] = (rgbArray[i] & 0xff000000) | c2;
}
}
} else {
for (int i = 0; i < len; i++) {
if (rgbArray[i] == c1) {
rgbArray[i] = c2;
}
}
}
image.setRGB(0, 0, width, height, rgbArray, 0, width);
}
/**
* Adjust brightness and contrast of the image using the adjustments
* indicated :
*
* - The brightness operand is a number in the range -255 .. 255 where
* larger valus make the image brighter and lower values darken the image. A
* value of 0 does not change the brightness
*
- The contrast operand is a positive float indicating the contrast
* enhancement. A value of 1.0 does not change the contrast. Values less
* than 1.0 lower the contrast while values higher than 1.0 enhance the
* contrast.
*
* The operation involved calculates the following value for each color of
* each pixel (the alpha channel is not modified) : destination = (
* source * contrast ) + brightness
The result is clipped to
* the range 0..255.
*
* @param brightness The brightness operand in the range -255 .. 255
* @param contrast The contrast factor in the rang 0.0f .. infinity
*/
public void adjust(int brightness, float contrast) {
// Check value ranges
if (brightness < -255) brightness = -255;
if (brightness > 255) brightness = 255;
if (contrast < 0.0f) contrast = 0.0f;
// Define the operation and do filter in place
RescaleOp rop = new RescaleOp(contrast, brightness, null);
BufferedImage image = getImageRGBA();
rop.filter(image, image);
}
/**
* Reduces the number of colors of the image to the indicated number, doing
* easy 'nearest' color replacement based on a weighting algorithm.
*
* @param numColors The number of colors to reduce the current image to.
*/
public void reduceColors(int numColors) {
DitherOp dither = new DitherOp(numColors, transparency, bgColor,
DitherOp.DITHER_NONE, null);
setImage(dither.filter(getImageRGBA(), null));
}
// ---------- drawing/painting settings
// -------------------------------------
/**
* Sets the paint for subsequent draw and fill operations. In its simplest
* case the paint will be a color to paint, but it can be any
* Paint
implementations such as GradientPaint
.
*
* The paint setting remains active until changed by the next setLineStyle()
* or setPaint() call.
*/
public void setPaint(Paint paint) {
g2.setPaint(paint);
}
public Paint getPaint() {
return g2.getPaint();
}
/**
* Sets the line stroke to use for the subsequent draw operation. A stroke
* defines how the outline of a shape is drawn, e.g. solid, dashed, etc.,
* and how the lines are terminated or interconnected.
*
* The stroke setting remains active until changed by the next
* setLineStyle() or setStroke() call.
*/
public void setStroke(Stroke stroke) {
g2.setStroke(stroke);
}
public Stroke getStroke() {
return g2.getStroke();
}
/**
* Sets both the paint and the stroke for the next draw and fill operations.
*
* The paint and stroke settings remain active until changed by the next
* setLineStyle() or setPaint() or setStroke() call.
*/
public void setLineStyle(LineStyle lineStyle) {
g2.setPaint(lineStyle);
g2.setStroke(lineStyle);
}
/**
* Sets the drawing composite for drawing and filling operations to the
* composite given. Most commonly the composite will be one of the
* predefined AlphaComposite
implementations.
*
* @param composite The composite to set for the drawing operations.
* @throws NullPointerException if composite is null
.
*/
public void setComposite(Composite composite) {
g2.setComposite(composite);
}
/**
* Returns the current Composite in the Graphics2D context.
*
* @return the current Graphics2D Composite, which defines a compositing
* style.
*/
public Composite getComposite() {
return g2.getComposite();
}
/**
* Sets the affine transformation applied to all of the drawing and filling
* operations. Setting the affine transformation this way you can apply
* rotation, resizing and combinations thereof to single objects to be drawn
* instead of having to rotate or resize the complete layer.
*
* @param transfrom The affine transformation to apply to all drawing and
* filling operations when operating on the layer.
*/
public void setTransform(AffineTransform transfrom) {
g2.setTransform(transfrom);
}
/**
* Define the set of color weights to use in luminance definition
*
* @param system The luminance vector to use.
* @see #GAMMA22
* @see #LINEAR
*/
public void setLuminanceSystem(LuminanceSystem system) {
if (system != null) {
this.lumSys = system;
}
}
/**
* Sets the value of a single preference for the rendering algorithms. Hint
* categories include controls for rendering quality and overall
* time/quality trade-off in the rendering process. Refer to the
* RenderingHints class for definitions of some common keys and values.
*
* @param hintKey the key of the hint to be set.
* @param hintValue the value indicating preferences for the specified hint
* category.
*/
public void setRenderingHint(RenderingHints.Key hintKey, Object hintValue) {
g2.setRenderingHint(hintKey, hintValue);
}
/**
* Returns the value of a single preference for the rendering algorithms.
* Hint categories include controls for rendering quality and overall
* time/quality trade-off in the rendering process. Refer to the
* RenderingHints class for definitions of some common keys and values.
*
* @param hintKey the key corresponding to the hint to ge
* @return an object representing the value for the specified hint key. Some
* of the keys and their associated values are defined in the
* RenderingHints class.
*/
public Object getRenderingHint(RenderingHints.Key hintKey) {
return g2.getRenderingHint(hintKey);
}
// ---------- draw and fill operations
// --------------------------------------
/**
* Render the given text string in the given font to the Layer
* using the attributes given. Use the default values given for unspecified
* values.
*
* If the width of the text box is set to some value other than zero, the
* text is broken to fit lines of the given length. The line breaking
* algorithm breaking algorithm breaks on whitespace (blank, tab), carriage
* return and linefeed (CR/LF) in any combination as well as on other
* characters such as hyphens.
*
* If the text contains carriage return and/or linefeed characters, the text
* is broken into several lines, regardless of whether the text would be
* broken because of the text box width setting.
*
* The layer may be enlarged to accomodate for the space needed for the text
* to be drawn. This is not the same behaviour as standard Java2D text
* drawing, where the text is clipped at the edges of the image.
*
* @param x left edge of the text box relative to the layer.
* @param y top edge of the text box relative to the layer.
* @param width maximum width of the textbox. If 0 (or negative) the width
* of the bounding box is dependent of the rendering attributes
* @param height maximum height of the textbox. If 0 (or negative) the width
* of the bounding box is dependent of the rendering attributes
* @param text the text string to draw
* @param font the font to render the text string
* @param align alignment, rotation and TrueType attributes for the text.
* Use {@link Font#ALIGN_LEFT} as default value.
* @param cs extra intercharacter spacing. Use 0 as default value to not add
* additional space.
* @param ls extra line spacing. Use 0 as default value to not add
* additional space.
* @return the number of text lines drawn to the layer
* @see Font#drawText
* @throws NullPointerException if either the text or the font argument is
* null
.
* @throws IllegalArgumentException if either x or y are negative.
*/
public int drawText(int x, int y, int width, int height, String text,
AbstractFont font, int align, double cs, int ls) {
return font.drawText(this, x, y, width, height, text, g2.getPaint(),
g2.getStroke(), align, cs, ls);
}
/**
* According to the current paint and composite setting, the given rectangle
* is drawn onto the layer. If the rect parameter is null
,
* the entire layer is filled with the paint.
*
* The edges of the rectangle drawn are defined as in Java2D, that is the
* left edge is x, the right edge is x+width-1, the top edge is y and the
* bottom edge is y+width-1.
*
* @param rect the rectangle to paint onto the layer. If set to null, the
* entire layer will be filled with that color.
*/
public void fillRect(Rectangle2D rect) {
if (rect == null) {
g2.fillRect(0, 0, width, height);
} else {
g2.fill(rect);
}
}
/**
* Fill the given rectangle with the layer defined as the temporary paint
* object. This obeys the composite setting, but ignores the current paint
* setting as the layer is taken as the paint. After the method has finished
* the current paint is set again.
*
* @param src the source Layer
to use. If this is the same as
* src
, nothin happens.
* @param rect the rectangle to paint onto the layer. If set to null, the
* entire layer will be filled with that color.
* @throws NullPointerException if src
is null
.
*/
public void fillRect(Layer src, Rectangle2D rect) {
if (src == null) {
throw new NullPointerException("src");
}
if (src == this) {
// log info
return;
}
Paint tp = new TexturePaint(src.getImage(), new Rectangle(0, 0, src.width, src.height));
Paint oldPaint = g2.getPaint();
g2.setPaint(tp);
fillRect(rect);
g2.setPaint(oldPaint);
}
/**
* Draws the outline of the given rectangle with the preset color and stroke
* obeying the current composite setting. If rect is null
,
* this draws the outline on the edges of the layer.
*
* The edges of the rectangle drawn are defined as in Java2D, that is the
* left edge is x, the right edge is x+width, the top edge is y and the
* bottom edge is y+width.
*
* @param rect the rectangle to draw onto the layer. If set to null, the
* entire layer will be surrounded with a border line.
*/
public void drawRect(Rectangle2D rect) {
if (rect == null) {
g2.drawRect(0, 0, width - 1, height - 1);
} else {
g2.draw(rect);
}
}
/**
* Draws a straight line between the points (x1/y1) and (x2/y2) in the
* current stroke and paint mode applying the current composite.
*
* @param x1 The x coordinate of the starting point
* @param y1 The y coordinate of the starting point
* @param x2 The x coordinate of the ending point
* @param y2 The y coordinate of the ending point
*/
public void drawLine(float x1, float y1, float x2, float y2) {
g2.draw(new Line2D.Float(x1, y1, x2, y2));
}
/**
* Draws a connected lines of multiple points given in the
* points
array. Each entry in the array contains of a x/y
* coordinate pair denoting one point in the line.
*
* @param points Array of x/y coordinate pairs. The array must consist of at
* least two entries, each entry containing at least two elements
* where the first element is interpreted as the x- and the
* second element is interpreted as the y-coordinate of that
* entry's point.
*/
public void drawPolyLine(float[][] points) {
GeneralPath shape = new GeneralPath();
shape.moveTo(points[0][0], points[0][1]);
for (int i = 1; i < points.length; i++) {
shape.lineTo(points[i][0], points[i][1]);
}
g2.draw(shape);
}
/**
* Draws an ellipse around the given center with the indicated horizontal
* and vertical radii. The ellipse is drawn filling a rectangle that is
* twice the vertical radius high and twice the horizontal radius wide. The
* center of this rectangle is designated by the center coordinates. To draw
* a circle define the horizontal and vertical radius to be the same value.
*
* @param cx The x coordinate of the ellipse center
* @param cy The y coordinate of the ellipse center
* @param a The horizontal radius of the ellipse
* @param b The vertical radius of the ellipse
*/
public void drawEllipse(float cx, float cy, float a, float b) {
g2.draw(new Ellipse2D.Double(cx - a, cy - b, a * 2, b * 2));
}
/**
* Fills an ellipse around the given center with the indicated horizontal
* and vertical radii. The ellipse is drawn filling a rectangle that is
* twice the vertical radius high and twice the horizontal radius wide. The
* center of this rectangle is designated by the center coordinates. To fill
* a circle define the horizontal and vertical radius to be the same value.
*
* @param cx The x coordinate of the ellipse center
* @param cy The y coordinate of the ellipse center
* @param a The horizontal radius of the ellipse
* @param b The vertical radius of the ellipse
*/
public void fillEllipse(float cx, float cy, float a, float b) {
g2.fill(new Ellipse2D.Double(cx - a, cy - b, a * 2, b * 2));
}
/**
* Draws an arc of the given ellipse. The base ellipse is defined as for
* {@link #drawEllipse(float, float, float, float)}, while only a segment
* of this ellipse is drawn. The segement is defined by the from and to
* angle denoting the starting and ending angle specified in degrees. The
* angle are defined such, that 0 degrees is the axis from the center to the
* right edge and positive angles turn counter clockwise.
*
* @param cx The x coordinate of the base ellipse center
* @param cy The y coordinate of the base ellipse center
* @param a The horizontal radius of the base ellipse
* @param b The vertical radius of the base ellipse
* @param from The starting angle as defined above
* @param extent The angle the arc spans as defined above
*/
public void drawSegment(float cx, float cy, float a, float b, double from,
double extent) {
g2.draw(new Arc2D.Double(cx - a, cy - b, a * 2, b * 2, from, extent,
Arc2D.OPEN));
}
/**
* Draws an arc of the given ellipse with which is closed by drawing
* additional lines from the center point to the starting and ending points
* of the arc. The base ellipse is defined as for of this ellipse is drawn.
* The segement is defined by the from and to angle denoting the starting
* and ending angle specified in degrees. The angle are defined such, that 0
* degrees is the axis from the center to the right edge and positive angles
* turn counter clockwise.
*
* @param cx The x coordinate of the base ellipse center
* @param cy The y coordinate of the base ellipse center
* @param a The horizontal radius of the base ellipse
* @param b The vertical radius of the base ellipse
* @param from The starting angle as defined above
* @param extent The angle the arc spans as defined above
*/
public void drawSector(float cx, float cy, float a, float b, double from,
double extent) {
g2.draw(new Arc2D.Double(cx - a, cy - b, a * 2, b * 2, from, extent,
Arc2D.PIE));
}
/**
* Filles a sector of the given ellipse with which is closed by drawing
* additional lines from the center point to the starting and ending points
* of the arc. The base ellipse is defined as for
* {@link #drawEllipse(float, float, float, float)}, while only a segment
* of this ellipse is drawn. The segement is defined by the from and to
* angle denoting the starting and ending angle specified in degrees. The
* angle are defined such, that 0 degrees is the axis from the center to the
* right edge and positive angles turn counter clockwise.
*
* @param cx The x coordinate of the base ellipse center
* @param cy The y coordinate of the base ellipse center
* @param a The horizontal radius of the base ellipse
* @param b The vertical radius of the base ellipse
* @param from The starting angle as defined above
* @param extent The angle the arc spans as defined above
*/
public void fillSector(float cx, float cy, float a, float b, double from,
double extent) {
g2.fill(new Arc2D.Double(cx - a, cy - b, a * 2, b * 2, from, extent,
Arc2D.PIE));
}
/**
* Draws (the outline of) the given shape. This is the generalized method of
* the different draw methods above. Using this method you can draw
* virtually anything you can possibly define in terms of a
* Shape
.
*
* The Shape
interface forms the basis for all geometrical
* figures such as circles, rectangles, lines but also text and even
* constructive area geometry.
*
* @param shape The Shape
implementation object to draw
*/
public void draw(Shape shape) {
g2.draw(shape);
}
/**
* Fills (the area of) the given shape. This is the generalized method of
* the different fill methods above. Using this method you can fill
* virtually anything you can possibly define in terms of a
* Shape
.
*
* The Shape
interface forms the basis for all geometrical
* figures such as circles, rectangles, lines but also text and even
* constructive area geometry.
*
* @param shape The Shape
implementation object to draw
*/
public void fill(Shape shape) {
g2.fill(shape);
}
// ---------- miscellaneous
// -------------------------------------------------
/**
* After checking whether the desired coordinate lies within the layer it
* gets the ARGB value out of storage and returns that value
*
* @param x horizontal coordinate (0 at the left) of the pixel
* @param y vertical coordinate (0 at the top) of the pixel
* @return 0 if pixel lies outside the layer, else the ARGB value of the
* pixel.
*/
public int getPixel(int x, int y) {
return getImage().getRGB(x, y);
}
/**
* After checking whether the pixel lies within the layer, its color is set
* to the desired value. If the pixel lies outside the layer no value is set
* and false is returned.
*
* @param x horizontal coordinate (0 at the left) of the pixel
* @param y vertical coordinate (0 at the top) of the pixel
* @param color ARGB value for the pixel
*/
public void setPixel(int x, int y, long color) {
getImage().setRGB(x, y, (int) color);
}
/**
* Gets the bounding box of the image, that is the size of the rectangle
* spanning all pixels, that do not have the same color as the background
* color. This background color is first guessed using the
* {@link #getBackgroundColor()} method.
*
* @return The calculated bounding box based on the guessed background
* color.
* @see #getBoundingBox(Color)
*/
public Rectangle2D getBoundingBox() {
return getBoundingBox(getBackgroundColor());
}
/**
* Gets the bounding box of the image, that is the size of the rectangle
* spanning all pixels, that do not have the same color as the given color
* which is supposed to be the background color.
*
* The algorithm scans starting on each edge of the image rectangle and
* aborts the scan as soon as a non-background pixel is encountered.
*
* @param bgcolor The color value not being considered image color, ie
* background color.
* @return The calculated bounding box based on the guessed background
* color.
* @throws NullPointerException if bgcolor is null
.
*/
public Rectangle2D getBoundingBox(Color bgcolor) {
int[] pixels = getImageRGBA().getRGB(0, 0, width, height, null, 0, width);
int br = pixels.length - 1;
int bgcol = bgcolor.getRGB();
int top = 0;
int bottom = 0;
int left = 0;
int right = 0;
// return a layer sized rectangle for empty layers
if (br == 0) {
return new Rectangle(1, 1);
}
// scan from top
for (int i = 0; i <= br; i++) {
if (bgcol != pixels[i]) {
top = i / width;
break;
}
}
// scan from bottom
for (int i = br; i >= 0; i--) {
if (bgcol != pixels[i]) {
bottom = i / width + 1;
break;
}
}
// scan from left
for (int i = 0; i != br; i += width) {
if (i > br) i -= br;
if (bgcol != pixels[i]) {
left = i % width;
break;
}
}
// scan from right
for (int i = br; i != 0; i -= width) {
if (i < 0) i += br;
if (bgcol != pixels[i]) {
right = i % width + 1;
break;
}
}
// Return the rectangle
return new Rectangle(left, top, right, bottom);
}
/**
* Floods the background with the new fill color. The backrgound color is
* first guessed using the {@link #getBackgroundColor()} method.
*
* @param fillColor The color to replace the background pixels
* @param blur The maximal color distance to apply to a pixel to treat it as
* a background color pixel.
* @see #floodFill(Color, int, Color)
*/
public void floodFill(Color fillColor, int blur) {
floodFill(fillColor, blur, getBackgroundColor());
}
/**
* Starting from the four edges of the image towards the center, the method
* replaces background color by the fill color. Pixels are considered
* background if the have the same color as the background color or if there
* color has a distance from the backrgound color which is less than the
* blur value as per the following formula :
* blur < sqrt( dr^2 + dg^2 + db^2 )
where dr,
* dg and db are the difference of the red, blue and green component of the
* pixel and the background color, resp.
*
* As a side effect the transparency color is set to the fill color if the
* transparency was set to the backrgound color just replaced.
*
* @param fillColor The color teplace for the background color.
* @param blur The maximum color distance as defined above.
* @param bgColor The background color to be replaced.
* @throws NullPointerException if either fillColor or bgColor is
* null
.
*/
public void floodFill(Color fillColor, int blur, Color bgColor) {
// Convert the color values to int
int fc = fillColor.getRGB();
int bc = bgColor.getRGB();
// Return immediately if background and flood color are within blur
// distance
if (isColorNear(fc, bc, blur)) {
return;
}
// Flood border around bounding box with painted rectangles
Rectangle2D rect = getBoundingBox(bgColor);
int l = (int) rect.getMinX();
int r = (int) rect.getMaxX();
int t = (int) rect.getMinY();
int b = (int) rect.getMaxY();
// Set the fill color
Paint oldPaint = g2.getPaint();
g2.setPaint(fillColor);
// Upper segment less one line
g2.fillRect(0, 0, width, t);
// Lower segment less one line
g2.fillRect(0, b, width, height - b);
// Left segment
g2.fillRect(0, t, l, b);
// Right segment
g2.fillRect(r, t, width - r, b);
// Recursively flood inside the bounding box starting on every border
// side
BufferedImage image = getImageRGBA();
int[] pixels = image.getRGB(0, 0, width, height, null, 0, width);
// Get top and bottom border
int i = l + width * t;
int end = r + width * t;
int di = width * (b - t - 1);
while (i < end) {
if (isColorNear(pixels[i], bc, blur)) {
floodRecursive(pixels, i, fc, bc, blur);
}
if (isColorNear(pixels[i + di], bc, blur)) {
floodRecursive(pixels, i + di, fc, bc, blur);
}
i++;
}
// Get left and right border
i = l + width * t;
end = l + width * b;
di = r - l - 1;
while (i < end) {
if (isColorNear(pixels[i], bc, blur)) {
floodRecursive(pixels, i, fc, bc, blur);
}
if (isColorNear(pixels[i + di], bc, blur)) {
floodRecursive(pixels, i + di, fc, bc, blur);
}
i += width;
}
// set modified pixels
image.setRGB(0, 0, width, height, pixels, 0, width);
// Set the fill color to be the transparency color, if it was set
// to the backrgound color
if (transparency == bgColor) transparency = fillColor;
// reset old paint
g2.setPaint(oldPaint);
}
/**
* Draws the image onto the layers image at the indicated position doing the
* image operation before drawing. Note that the x and y coordinates are
* relative to the layers image and not relative to the x/y coordinates of
* the layer itself. That is, the coordinate 0/0 draws at the top left
* corner of the layer regardless of the value of the layers x/y value.
*
* @param img The image to draw
* @param op The BufferedImageOp
to execute on the image
* before drawing, if null
the image is drawn
* unaltered.
* @param x The left position to draw the image to
* @param y The top position to draw the image to
*/
public void drawImage(BufferedImage img, BufferedImageOp op, int x, int y) {
g2.drawImage(img, op, x, y);
}
/**
* Draws the layer into another graphics element, for example another image
* or a canvas. The image is drawn at the position specified by the left and
* top edge of the layer.
*
* @param g The graphics element to draw into
*/
public void draw(Graphics g) {
g.drawImage(getImage(), 0, 0, null);
}
// ---------- beany stuff
// ---------------------------------------------------
/**
* Returns the left edge of the layer. This value is relevant, if the layer
* is to be merged with other layers, to position the layers relative to
* each other.
*
* @return left edge of the layer.
*/
public int getX() {
return x;
}
/**
* Sets the left edge of the layer. This value is relevant, if the layer is
* to be merged with other layers, to position the layers relative to each
* other.
*
* @param x left edge of the layer.
*/
public void setX(int x) {
this.x = x;
}
/**
* Returns the top edge of the layer. This value is relevant, if the layer
* is to be merged with other layers, to position the layers relative to
* each other.
*
* @return top edge of the layer.
*/
public int getY() {
return y;
}
/**
* Sets the top edge of the layer. This value is relevant, if the layer is
* to be merged with other layers, to position the layers relative to each
* other.
*
* @param y top edge of the layer.
*/
public void setY(int y) {
this.y = y;
}
/**
* Returns the width of the layer.
*
* @return the width of the layer.
*/
public int getWidth() {
return width;
}
/**
* Returns the height of the layer.
*
* @return the height of the layer.
*/
public int getHeight() {
return height;
}
/**
* Returns the rectangle spanned by the image. This rectangle encompasses
* the complete image where as {@link #getBoundingBox} returns the smallest
* area of the image not only covered by the background color.
*/
public Rectangle getBounds() {
return getImage().getRaster().getBounds();
}
/**
* Returns the index of image of this layer within the multi-image file from
* which this layer has been loaded. This field will only be non-zero if the
* layer has been created by a call to the {@link #Layer(InputStream, int)}
* constructor with a non-zero index.
*
* @return The index of the image of this layer in the image source file.
*/
public int getImageIndex() {
return imageIndex;
}
/**
* Returns the number of images in the source from where this layer has been
* loaded or -1 if the number of images is not known or the lay has not been
* loaded from an image file.
*
* @return The number of images in the source file or -1 if not known.
*/
public int getNumImages() {
return numImages;
}
/**
* Returns the background of the layer.
*
* @return the background of the layer.
*/
public Paint getBackground() {
return backGround;
}
/**
* Sets the background of the layer. If the background is a
* Color
the background color ({@link #getBackgroundColor()})
* is also set.
*
* @param bground The new background.
*/
public void setBackground(Paint bground) {
this.backGround = (bground != null)
? bground
: TRANSPARENT_IMAGE_BACKGROUND;
if (this.backGround instanceof Color) {
this.bgColor = (Color) this.backGround;
}
}
/**
* Set the background color to the indicated value. This value is
* overwritten by {@link #setBackground(Paint)} and potentially also by
* {@link #getBackgroundColor()}.
*
* You may set the background color to null
, if you want to
* recalculate the background color based on the image data the next time
* you call {@link #getBackgroundColor()}.
*
* @param bgColor The Color
to set for the background or
* null
to force recalculation by the next
* {@link #getBackgroundColor()} call.
*/
public void setBackgroundColor(Color bgColor) {
this.bgColor = bgColor;
}
/**
* Gets the background color of the image. If the background color has not
* been set by {@link #setBackgroundColor(Color)}, the background color is
* taken to be the background ({@link #getBackground()} if the background
* is a color, else the color is calculated to be the color most often
* occuring on the edges of the layer. This may not be 100% correct but gets
* acceptable results.
*
* @return The supposed background color.
*/
public Color getBackgroundColor() {
if (bgColor == null) {
if (backGround instanceof Color) {
bgColor = (Color) backGround;
} else {
// Color counters
long[] cs = new long[2 * width + 2 * height];
int[] numCs = new int[2 * width + 2 * height];
int cols = -1;
// Analize top and bottom border
BufferedImage image = getImageRGBA();
int[] rowT = image.getRGB(0, 0, width, 1, null, 0, width);
int[] rowB = image.getRGB(0, height - 1, width, 1, null, 0,
width);
for (int i = 0; i < rowT.length; i++) {
long c1 = rowT[i] & 0xffffffffL;
long c2 = rowB[i] & 0xffffffffL;
for (int j = 0; j <= cols; j++) {
if (c1 == cs[j]) {
numCs[j]++;
c1 = -1;
}
if (c2 == cs[j]) {
numCs[j]++;
c2 = -1;
}
}
if (c1 >= 0) cs[++cols] = c1;
if (c2 >= 0) cs[++cols] = c2;
}
// Analize left and right border
int[] colL = image.getRGB(0, 0, 1, height, null, 0, 1);
int[] colR = image.getRGB(width - 1, 0, 1, height, null, 0, 1);
for (int i = 0; i < colL.length; i++) {
long c1 = colL[i] & 0xffffffffL;
long c2 = colR[i] & 0xffffffffL;
for (int j = 0; j <= cols; j++) {
if (c1 == cs[j]) {
numCs[j]++;
c1 = -1;
}
if (c2 == cs[j]) {
numCs[j]++;
c2 = -1;
}
}
if (c1 >= 0) cs[++cols] = c1;
if (c2 >= 0) cs[++cols] = c2;
}
// Get the most often color
int max = 0;
int maxOcc = numCs[0];
for (int i = 1; i < cols; i++) {
if (numCs[i] > maxOcc) {
max = i;
maxOcc = numCs[i];
}
}
// That's it
bgColor = new Color((int) cs[max], true);
}
}
return bgColor;
}
/**
* Returns the Color
value of transparent pixels. This is
* prominently used by the GIF image format, which does not know about alpha
* values. For GIF images, a color may be specified to identify pixels which
* should be show transparent.
*
* @return the Color
value of transparent pixels
*/
public Color getTransparency() {
return transparency;
}
/**
* Sets the Color
value of pixels to be regarded transparent.
* This is prominently used by the GIF image format, which does not know
* about alpha values. For GIF images, a color may be specified to identify
* pixels which should be show transparent.
*
* @param transparency The transparency color.
*/
public void setTransparency(Color transparency) {
this.transparency = transparency;
}
/**
* Sets the MIME type of the image, which is used when writing the image
* with no explicite MIME type setting.
*
* @param mimeType The MIME type to set on the layer. If null
* or empty, the currently set MIME type is not changed.
*/
public void setMimeType(String mimeType) {
if (mimeType != null && mimeType.length() > 0) {
this.mimeType = mimeType;
}
}
/**
* Returns the MIME type of the image from which the layer has been loaded.
* If the layer is not created from an image, the method returns the default
* MIME type image/gif
.
*
* @return the MIME type assigned to the layer.
*/
public String getMimeType() {
return mimeType;
}
/**
* Gets the opacity of the layer. The opacity value is used when merging
* layers to define the relative transparency of two layers merged.
*
* @return the layer's opacity.
* @see #setOpacity
*/
public float getOpacity() {
return opacity;
}
/**
* Sets the opacity of the layer. The opacity value is used when merging
* layers to define the relative transparency of two layers merged.
*
* @param opacity the layer's opacity. If less than zero, zero is set; if
* higher than 1.0, 1.0 is set; if not a number (Float.NaN
)
* the opacity is not changed.
* @see #getOpacity
*/
public void setOpacity(float opacity) {
if (!Float.isNaN(opacity)) {
// check bounds
if (opacity > 1) opacity = 1;
if (opacity < 0) opacity = 0;
this.opacity = opacity;
}
}
/**
* Returns the current Composite
of this layer. The default
* composite set is AlphaComposite.SrcOver
.
*
* This method returns the Composite
which is set on this
* layer and which is used for layer merging. This is different from the
* Composite
returned from the {@link #getComposite} method,
* which returns the Composite
used for drawing operations
* until reset.
*
* @return The current Composite
.
* @see #setLayerComposite
*/
public Composite getLayerComposite() {
return layerComposite;
}
/**
* Sets the AlphaComposite
used for merging this layer onto
* other layers.
*
* This method sets the Composite
on this layer which is used
* for layer merging. This is different from the Composite
* set through {@link #setComposite}, which sets the Composite
* used for drawing operations until reset.
*
* @param layerComposite The Composite
to set on this layer.
* If null
the current Composite
is
* not changed.
* @see #getLayerComposite
*/
public void setLayerComposite(Composite layerComposite) {
if (layerComposite != null) {
this.layerComposite = layerComposite;
}
}
/**
* Returns the image on which this layer is based. This may not be
* an RGBA image and may even have a custom color space attached.
*
* @return The image on which this layer is based.
*/
public BufferedImage getImage() {
return baseImg;
}
/**
* Returns the image as an RGBA image and as a side effect replaces
* the current base image of this layer by the converted image.
*/
private BufferedImage getImageRGBA() {
if (!baseImgIsRGBA) {
BufferedImage newImage = new BufferedImage(baseImg.getWidth(), baseImg.getHeight(), IMAGE_TYPE);
Graphics2D g2 = newImage.createGraphics();
g2.drawRenderedImage(baseImg, IDENTITY_XFORM);
g2.dispose();
setImage(newImage);
}
return baseImg;
}
/**
* Returns the Graphics2D
object used to draw into this
* layer.
*
* @return the Graphics2D
object used to draw into this
* layer.
*/
public Graphics2D getG2() {
return g2;
}
/**
* Replaces the current base image by a new image. The dimension of the
* layer is adpated to the dimension of the new layer. All other properties
* of the layer - like left and top edge, background, background Color, etc. -
* are not modified.
*
* @param image The image to set in the layer. This image should be
* BufferedImage
with the image type set to
* BufferedImage.TYPE_4BYTE_ABGR
.
*/
void setImage(BufferedImage image) {
setImage(image, false);
}
/**
* Replaces the current base image by a new image. The dimension of the
* layer is adpated to the dimension of the new layer. All other properties
* of the layer - like left and top edge, background, background Color, etc. -
* are not modified.
*
* @param image The image to set in the layer. This image should be
* BufferedImage
with the image type set to
* BufferedImage.TYPE_4BYTE_ABGR
.
* @param paintBackground Whether to fill the image with the background
* color or not.
*/
private void setImage(BufferedImage image, boolean paintBackground) {
if (image != null && image != baseImg) {
if (baseImg != null) {
baseImg.flush();
}
if (g2 != null) {
g2.dispose();
}
baseImg = image;
baseImgIsRGBA = baseImg.getType() == BufferedImage.TYPE_4BYTE_ABGR;
// adapt dimension
width = baseImg.getWidth();
height = baseImg.getHeight();
// init g2
g2 = baseImg.createGraphics();
setRenderingHints(g2);
// clear to background
g2.setBackground((backGround instanceof Color)
? (Color) backGround
: Color.white);
if (paintBackground) {
g2.setPaint(backGround);
g2.fillRect(0, 0, width, height);
}
}
}
// ---------- Object overwrite
// ----------------------------------------------
/**
* Convert the Layer to some string representation for intelligent display.
*
* @return the String
representation of the
* Layer
.
*/
public String toString() {
return "Layer: left=" + x + ", top=" + y + ", width=" + width
+ ", height=" + height + ", background=" + backGround + ", mime="
+ mimeType;
}
// ---------- Private helpers
// -----------------------------------------------
/**
* Initializes the layer object in the same way for the different
* constructors. The layer is initialized to the bground
. If
* the bground
is a Color, the background color of the basing
* graphics object is set to that color, else the background color is set to
* opaque white (Color.WHITE
).
*
* If the layer is already back by an image, the existing image is
* replaced by a new image while the old image is flushed and the
* corresponding Graphics2D is disposed off.
*
* @param width Width of the new layer
* @param height Height of the new layer
* @param bground The background of the image, generally a
* Color
, if null
the default
* background is transparent white.
* @throws IllegalArgumentException if the height or the width of the layer
* is negative or zero.
*/
private void init(int width, int height, Paint bground) {
if (width <= 0 || height <= 0) {
throw new IllegalArgumentException("width or height <= 0");
}
// Get global properties
this.x = DEFAULT_IMAGE_ORIGINX;
this.y = DEFAULT_IMAGE_ORIGINY;
// this.height -- set in initBaseImage()
// this.width -- set in initBaseImage()
setBackground(bground);
this.opacity = DEFAULT_IMAGE_OPACITY;
this.transparency = null;
this.mimeType = DEFAULT_MIME_TYPE;
this.layerComposite = DEFAULT_LAYER_COMPOSITE;
// set the image
setImage(new BufferedImage(width, height, IMAGE_TYPE), true);
}
/**
* Sets the standard rendering hints to the graphics
*
* @param g2 The graphics to set the rendering hints to
*/
private void setRenderingHints(Graphics2D g2) {
// Set rendering hints. These may fail - don't know why ??
if (RENDERING_HINTS == null) {
Map tmp = new HashMap(7);
tmp.put(RenderingHints.KEY_RENDERING,
RenderingHints.VALUE_RENDER_QUALITY);
tmp.put(RenderingHints.KEY_COLOR_RENDERING,
RenderingHints.VALUE_COLOR_RENDER_QUALITY);
tmp.put(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_OFF);
tmp.put(RenderingHints.KEY_TEXT_ANTIALIASING,
RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);
tmp.put(RenderingHints.KEY_ALPHA_INTERPOLATION,
RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
tmp.put(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BICUBIC);
tmp.put(RenderingHints.KEY_FRACTIONALMETRICS,
RenderingHints.VALUE_FRACTIONALMETRICS_ON);
RENDERING_HINTS = tmp;
}
g2.setRenderingHints(RENDERING_HINTS);
}
/**
* Check the rectangle whether it fits the layer and it is not null:
*
* null
* return a new rectangle spanning the layer
*
* inside layer
* return the recangle
*
* partly outside
* return a new rectangle clipped to the layer
*
*
* @param rect The rectangle to check
* @return A valid rectangle being fully inside the layer
*/
private Rectangle2D checkRect(Rectangle2D rect) {
if (rect == null) {
return new Rectangle2D.Float(x, y, x + width, y + height);
} else {
double rx = rect.getX();
double ry = rect.getY();
double rw = rect.getWidth();
double rh = rect.getHeight();
if (rx < x) rx = x;
if (ry < y) ry = y;
if (rx + rw > x + width)
rw = x + width - rx;
else if (rw == 0) rw = 1;
if (ry + rh > y + height)
rh = y + height - ry;
else if (rh == 0) rh = 1;
rect.setRect(rx, ry, rw, rh);
return rect;
}
}
/**
* Helper method to apply a affine transform to the image, returning the new
* BufferedImage
.
*
* @param newImage the BufferedImage
into which the
* transformed image is drawn.
* @param xform the AffineTransform
to apply to the image
*/
private void transformImage(BufferedImage newImage, AffineTransform xform) {
Graphics2D newG2 = newImage.createGraphics();
setRenderingHints(newG2);
if (xform != null) {
newG2.drawRenderedImage(getImage(), xform);
}
setImage(newImage);
}
/**
* Calculate the color distance of the two color values. The color distance
* of two color values is defined as dist = sqrt( dr^2 + dg^2 + db^2 )
*
* @param col1 The one color
* @param col2 The other color
* @param maxDist The maximum allowed distance
*
* @return true
if the distance between the colors is less
* than or equal to maxDist
*/
private boolean isColorNear(int col1, int col2, long maxDist) {
if (col1 == col2) {
return true;
} else {
// we have to do alpha scaling
int a1 = (col1 >>> 24) & 0xff;
int a2 = (col2 >>> 24) & 0xff;
long r = a1 * ((col1 >>> 16) & 0xff) - a2 * ((col2 >>> 16) & 0xff);
long g = a1 * ((col1 >>> 8) & 0xff) - a2 * ((col2 >>> 8) & 0xff);
long b = a1 * ((col1) & 0xff) - a2 * ((col2) & 0xff);
return (r * r + g * g + b * b) <= (maxDist * maxDist * 255 * 255);
}
}
/**
* Recurse the flood fill into the image. Recursion may be get too deep. For
* this reason we catch the StackOverflowError and return if occurring. It
* may prove, that the recursive implementation is sub- optimal.
*
* @param pixels The image pixels
* @param pos The current position to fill
* @param fillCol The fill color
* @param bgCol The original background color
* @param blur The blur factor - the maximum color distance to still fill
*/
private void floodRecursive(int[] pixels, int pos, int fillCol, int bgCol,
int blur) {
// degress blur into the distance
int j = (blur > 0) ? (blur / 2) : 0;
int pix = pixels[pos];
// Set the pixel to the fill color
if (bgCol == pix) {
pixels[pos] = fillCol;
} else if (blur > 0) {
// blur the fill color into the pixel
int pr = (pix >>> 16) & 0xff;
int pg = (pix >>> 8) & 0xff;
int pb = (pix) & 0xff;
int br = (bgCol >>> 16) & 0xff;
int bg = (bgCol >>> 8) & 0xff;
int bb = (bgCol) & 0xff;
int dr = pr - br;
int dg = pg - bg;
int db = pb - bb;
int aq = (int) Math.sqrt(dr * dr + dg * dg + db * db);
int ai = blur - aq;
int r = (pr * aq - br * ai) / blur;
int g = (pg * aq - bg * ai) / blur;
int b = (pb * aq - bb * ai) / blur;
pixels[pos] = pix & 0xff000000 + (r << 16) + (g << 8) + b;
}
// define neighbours
int[] o = new int[4];
int i = 0;
int x = pos % width;
int y = pos / width;
if (x + 1 < width) o[i++] = pos + 1; /* right */
if (x > 0) o[i++] = pos - 1; /* left */
if (y > 0) o[i++] = pos - width; /* upper */
if (y + 1 < height) o[i++] = pos + width; /* lower */
// check those neighbours
while (i > 0) {
i--;
if (isColorNear(pixels[o[i]], bgCol, blur)) {
try {
floodRecursive(pixels, o[i], fillCol, bgCol, j);
} catch (StackOverflowError soe) {
// log or throw ???
return;
}
}
}
}
// ---------- static member class
// -------------------------------------------
/**
* The LuminanceSystem
class encapsulates luminance factor
* values for greyscaling operations.
*/
public static final class LuminanceSystem {
private final String name;
private final float r;
private final float g;
private final float b;
private String stringRep;
LuminanceSystem(String name, float r, float g, float b) {
this.name = name;
this.r = r;
this.g = g;
this.b = b;
this.stringRep = null;
}
LuminanceSystem(float r, float g, float b) {
this(null, r, g, b);
}
float r() {
return r;
}
float g() {
return g;
}
float b() {
return b;
}
public String toString() {
if (stringRep == null) {
StringBuffer buf = new StringBuffer();
if (name != null) {
buf.append(name);
buf.append(' ');
}
stringRep = buf.append('[').append(r).append(',').append(g).append(
',').append(b).append(']').toString();
}
return stringRep;
}
}
}