All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.day.image.Layer Maven / Gradle / Ivy

/*************************************************************************
 *
 * 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.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 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 { /** * 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 the Sun * JPEG decoder. 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, 1024) { // 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(1024); // 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) { // failed reading ... should log imageReadFailure = e; } } if (imageReadFailure != null) { // try next reader, if any if (readers.hasNext()) { reader.dispose(); ios.reset(); continue; } // no more readers, rethrow this throw imageReadFailure; } else 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 * NullPointerExceptions. */ 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 Layers 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 Layers 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; } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy