diff --git a/twelvemonkeys-core/pom.xml b/twelvemonkeys-core/pom.xml
new file mode 100644
index 00000000..86735787
--- /dev/null
+++ b/twelvemonkeys-core/pom.xml
@@ -0,0 +1,58 @@
+
+
+ 4.0.0
+ twelvemonkeys-core
+ com.twelvemonkeys
+ 2.2-SNAPSHOT
+ TwelveMonkeys Core
+
+ The TwelveMonkeys Core library. Contains common utility classes.
+
+
+
+ com.twelvemonkeys
+ twelvemonkeys-parent
+ 2.0
+
+
+
+
+ jmagick
+ jmagick
+ 6.2.4
+ provided
+ true
+
+
+
+ junit
+ junit
+ 4.3.1
+ test
+
+
+
+ jmock
+ jmock-cglib
+ 1.0.1
+ test
+
+
+
+
+
+
+ maven-source-plugin
+
+
+
+ maven-resources-plugin
+
+ UTF-8
+
+
+
+
+
\ No newline at end of file
diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/AbstractImageSource.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/AbstractImageSource.java
new file mode 100755
index 00000000..7b8a3b46
--- /dev/null
+++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/AbstractImageSource.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (c) 2008, Harald Kuhr
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * Neither the name "TwelveMonkeys" nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.twelvemonkeys.image;
+
+import java.awt.image.ImageProducer;
+import java.awt.image.ImageConsumer;
+import java.util.List;
+import java.util.ArrayList;
+
+/**
+ * AbstractImageSource
+ *
+ *
+ * @author Harald Kuhr
+ * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/AbstractImageSource.java#1 $
+ */
+public abstract class AbstractImageSource implements ImageProducer {
+ private List mConsumers = new ArrayList();
+ protected int mWidth;
+ protected int mHeight;
+ protected int mXOff;
+ protected int mYOff;
+
+ // ImageProducer interface
+ public void addConsumer(ImageConsumer pConsumer) {
+ if (mConsumers.contains(pConsumer)) {
+ return;
+ }
+ mConsumers.add(pConsumer);
+ try {
+ initConsumer(pConsumer);
+ sendPixels(pConsumer);
+ if (isConsumer(pConsumer)) {
+ pConsumer.imageComplete(ImageConsumer.STATICIMAGEDONE);
+
+ // Get rid of "sticky" consumers...
+ if (isConsumer(pConsumer)) {
+ pConsumer.imageComplete(ImageConsumer.IMAGEERROR);
+ removeConsumer(pConsumer);
+ }
+ }
+ }
+ catch (Exception e) {
+ e.printStackTrace();
+ if (isConsumer(pConsumer)) {
+ pConsumer.imageComplete(ImageConsumer.IMAGEERROR);
+ }
+ }
+ }
+
+ public void removeConsumer(ImageConsumer pConsumer) {
+ mConsumers.remove(pConsumer);
+ }
+
+ /**
+ * This implementation silently ignores this instruction. If pixeldata is
+ * not in TDLR order by default, subclasses must override this method.
+ *
+ * @param pConsumer the consumer that requested the resend
+ *
+ * @see ImageProducer#requestTopDownLeftRightResend(java.awt.image.ImageConsumer)
+ */
+ public void requestTopDownLeftRightResend(ImageConsumer pConsumer) {
+ // ignore
+ }
+
+ public void startProduction(ImageConsumer pConsumer) {
+ addConsumer(pConsumer);
+ }
+
+ public boolean isConsumer(ImageConsumer pConsumer) {
+ return mConsumers.contains(pConsumer);
+ }
+
+ protected abstract void initConsumer(ImageConsumer pConsumer);
+
+ protected abstract void sendPixels(ImageConsumer pConsumer);
+}
diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/AreaAverageOp.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/AreaAverageOp.java
new file mode 100755
index 00000000..1ece4bd4
--- /dev/null
+++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/AreaAverageOp.java
@@ -0,0 +1,453 @@
+/*
+ * Copyright (c) 2008, Harald Kuhr
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * Neither the name "TwelveMonkeys" nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.twelvemonkeys.image;
+
+import javax.imageio.ImageIO;
+import javax.swing.*;
+import java.awt.*;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+import java.awt.image.*;
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * AreaAverageOp
+ *
+ * @author Harald Kuhr
+ * @author last modified by $Author: haku $
+ * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/AreaAverageOp.java#2 $
+ */
+public class AreaAverageOp implements BufferedImageOp, RasterOp {
+
+ final private int mWidth;
+ final private int mHeight;
+
+ private Rectangle mSourceRegion;
+
+ public AreaAverageOp(final int pWidth, final int pHeight) {
+ mWidth = pWidth;
+ mHeight = pHeight;
+ }
+
+ public Rectangle getSourceRegion() {
+ if (mSourceRegion == null) {
+ return null;
+ }
+ return new Rectangle(mSourceRegion);
+ }
+
+ public void setSourceRegion(final Rectangle pSourceRegion) {
+ if (pSourceRegion == null) {
+ mSourceRegion = null;
+ }
+ else {
+ if (mSourceRegion == null) {
+ mSourceRegion = new Rectangle(pSourceRegion);
+ }
+ else {
+ mSourceRegion.setBounds(pSourceRegion);
+ }
+ }
+ }
+
+ public BufferedImage filter(BufferedImage src, BufferedImage dest) {
+ BufferedImage result = dest != null ? dest : createCompatibleDestImage(src, null);
+
+ // TODO: src and dest can't be the same
+
+ // TODO: Do some type checking here..
+ // Should work with
+ // * all BYTE types, unless sub-byte packed rasters/IndexColorModel
+ // * all INT types (even custom, as long as they use 8bit/componnet)
+ // * all USHORT types (even custom)
+
+ // TODO: Also check if the images are really compatible!?
+
+ long start = System.currentTimeMillis();
+ // Straight-forward version
+ //Image scaled = src.getScaledInstance(mWidth, mHeight, Image.SCALE_AREA_AVERAGING);
+ //ImageUtil.drawOnto(result, scaled);
+ //result = new BufferedImageFactory(scaled).getBufferedImage();
+
+ /*
+ // Try: Use bilinear/bicubic and half the image down until it's less than
+ // twice as big, then use bicubic for the last step?
+ BufferedImage temp = null;
+ AffineTransform xform = null;
+ int w = src.getWidth();
+ int h = src.getHeight();
+ while (w / 2 > mWidth && h / 2 > mHeight) {
+ w /= 2;
+ h /= 2;
+
+ if (temp == null) {
+ xform = AffineTransform.getScaleInstance(.5, .5);
+ ColorModel cm = src.getColorModel();
+ temp = new BufferedImage(cm,
+ ImageUtil.createCompatibleWritableRaster(src, cm, w, h),
+ cm.isAlphaPremultiplied(), null);
+
+ resample(src, temp, xform);
+ }
+ else {
+ resample(temp, temp, xform);
+ }
+
+ System.out.println("w: " + w);
+ System.out.println("h: " + h);
+ }
+
+ if (temp != null) {
+ src = temp.getSubimage(0, 0, w, h);
+ }
+
+ resample(src, result, AffineTransform.getScaleInstance(mWidth / (double) w, mHeight / (double) h));
+ */
+
+ // The real version
+ filterImpl(src.getRaster(), result.getRaster());
+
+ long time = System.currentTimeMillis() - start;
+ System.out.println("time: " + time);
+
+ return result;
+ }
+
+ private void resample(final BufferedImage pSrc, final BufferedImage pDest, final AffineTransform pXform) {
+ Graphics2D d = pDest.createGraphics();
+ d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
+ try {
+ d.drawImage(pSrc, pXform, null);
+ }
+ finally {
+ d.dispose();
+ }
+ }
+
+ public WritableRaster filter(Raster src, WritableRaster dest) {
+ WritableRaster result = dest != null ? dest : createCompatibleDestRaster(src);
+ return filterImpl(src, result);
+ }
+
+ private WritableRaster filterImpl(Raster src, WritableRaster dest) {
+ //System.out.println("src: " + src);
+ //System.out.println("dest: " + dest);
+ if (mSourceRegion != null) {
+ int cx = mSourceRegion.x;
+ int cy = mSourceRegion.y;
+ int cw = mSourceRegion.width;
+ int ch = mSourceRegion.height;
+
+ boolean same = src == dest;
+ dest = dest.createWritableChild(cx, cy, cw, ch, 0, 0, null);
+ src = same ? dest : src.createChild(cx, cy, cw, ch, 0, 0, null);
+ //System.out.println("src: " + src);
+ //System.out.println("dest: " + dest);
+ }
+
+ final int width = src.getWidth();
+ final int height = src.getHeight();
+
+ // TODO: This don't work too well..
+ // The thing is that the step length and the scan length will vary, for
+ // non-even (1/2, 1/4, 1/8 etc) resampling
+ int widthSteps = (width + mWidth - 1) / mWidth;
+ int heightSteps = (height + mHeight - 1) / mHeight;
+
+ final boolean oddX = width % mWidth != 0;
+ final boolean oddY = height % mHeight != 0;
+
+ final int dataElements = src.getNumDataElements();
+ final int bands = src.getNumBands();
+ final int dataType = src.getTransferType();
+
+ Object data = null;
+ int scanW;
+ int scanH;
+
+ // TYPE_USHORT setup
+ int[] bitMasks = null;
+ int[] bitOffsets = null;
+ if (src.getTransferType() == DataBuffer.TYPE_USHORT) {
+ if (src.getSampleModel() instanceof SinglePixelPackedSampleModel) {
+ // DIRECT
+ SinglePixelPackedSampleModel sampleModel = (SinglePixelPackedSampleModel) src.getSampleModel();
+ bitMasks = sampleModel.getBitMasks();
+ bitOffsets = sampleModel.getBitOffsets();
+ }
+ else {
+ // GRAY
+ bitMasks = new int[]{0xffff};
+ bitOffsets = new int[]{0};
+ }
+ }
+
+ for (int y = 0; y < mHeight; y++) {
+ if (!oddY || y < mHeight) {
+ scanH = heightSteps;
+ }
+ else {
+ scanH = height - (y * heightSteps);
+ }
+
+ for (int x = 0; x < mWidth; x++) {
+ if (!oddX || x < mWidth) {
+ scanW = widthSteps;
+ }
+ else {
+ scanW = width - (x * widthSteps);
+ }
+ final int pixelCount = scanW * scanH;
+ final int pixelLength = pixelCount * dataElements;
+
+ try {
+ data = src.getDataElements(x * widthSteps, y * heightSteps, scanW, scanH, data);
+ }
+ catch (IndexOutOfBoundsException e) {
+ // TODO: FixMe!
+ // The bug is in the steps...
+ //System.err.println("x: " + x);
+ //System.err.println("y: " + y);
+ //System.err.println("widthSteps: " + widthSteps);
+ //System.err.println("heightSteps: " + heightSteps);
+ //System.err.println("scanW: " + scanW);
+ //System.err.println("scanH: " + scanH);
+ //
+ //System.err.println("width: " + width);
+ //System.err.println("height: " + height);
+ //System.err.println("mWidth: " + mWidth);
+ //System.err.println("mHeight: " + mHeight);
+ //
+ //e.printStackTrace();
+ continue;
+ }
+
+ // TODO: Might need more channels... Use an array?
+ // NOTE: These are not neccessarily ARGB..
+ double valueA = 0.0;
+ double valueR = 0.0;
+ double valueG = 0.0;
+ double valueB = 0.0;
+
+ switch (dataType) {
+ case DataBuffer.TYPE_BYTE:
+ // TODO: Doesn't hold for index color models...
+ // For index color, the best bet is probably convert to
+ // true color, then convert back to the same index color
+ // model
+ byte[] bytePixels = (byte[]) data;
+ for (int i = 0; i < pixelLength; i += dataElements) {
+ valueA += bytePixels[i] & 0xff;
+ if (bands > 1) {
+ valueR += bytePixels[i + 1] & 0xff;
+ valueG += bytePixels[i + 2] & 0xff;
+ if (bands > 3) {
+ valueB += bytePixels[i + 3] & 0xff;
+ }
+ }
+ }
+
+ // Average
+ valueA /= pixelCount;
+ if (bands > 1) {
+ valueR /= pixelCount;
+ valueG /= pixelCount;
+ if (bands > 3) {
+ valueB /= pixelCount;
+ }
+ }
+
+ //for (int i = 0; i < pixelLength; i += dataElements) {
+ bytePixels[0] = (byte) clamp((int) valueA);
+ if (bands > 1) {
+ bytePixels[1] = (byte) clamp((int) valueR);
+ bytePixels[2] = (byte) clamp((int) valueG);
+ if (bands > 3) {
+ bytePixels[3] = (byte) clamp((int) valueB);
+ }
+ }
+ //}
+ break;
+
+ case DataBuffer.TYPE_INT:
+ int[] intPixels = (int[]) data;
+ // TODO: Rewrite to use bit offsets and masks from
+ // color model (see TYPE_USHORT) in case of a non-
+ // 888 or 8888 colormodel?
+ for (int i = 0; i < pixelLength; i += dataElements) {
+ valueA += (intPixels[i] & 0xff000000) >> 24;
+ valueR += (intPixels[i] & 0xff0000) >> 16;
+ valueG += (intPixels[i] & 0xff00) >> 8;
+ valueB += (intPixels[i] & 0xff);
+ }
+
+ // Average
+ valueA /= pixelCount;
+ valueR /= pixelCount;
+ valueG /= pixelCount;
+ valueB /= pixelCount;
+
+ //for (int i = 0; i < pixelLength; i += dataElements) {
+ intPixels[0] = clamp((int) valueA) << 24;
+ intPixels[0] |= clamp((int) valueR) << 16;
+ intPixels[0] |= clamp((int) valueG) << 8;
+ intPixels[0] |= clamp((int) valueB);
+ //}
+ break;
+
+ case DataBuffer.TYPE_USHORT:
+ if (bitMasks != null) {
+ short[] shortPixels = (short[]) data;
+ for (int i = 0; i < pixelLength; i += dataElements)
+ {
+ valueA += (shortPixels[i] & bitMasks[0]) >> bitOffsets[0];
+ if (bitMasks.length > 1) {
+ valueR += (shortPixels[i] & bitMasks[1]) >> bitOffsets[1];
+ valueG += (shortPixels[i] & bitMasks[2]) >> bitOffsets[2];
+ if (bitMasks.length > 3) {
+ valueB += (shortPixels[i] & bitMasks[3]) >> bitOffsets[3];
+ }
+ }
+ }
+
+ // Average
+ valueA /= pixelCount;
+ valueR /= pixelCount;
+ valueG /= pixelCount;
+ valueB /= pixelCount;
+
+ //for (int i = 0; i < pixelLength; i += dataElements) {
+ shortPixels[0] = (short) (((int) valueA << bitOffsets[0]) & bitMasks[0]);
+ if (bitMasks.length > 1) {
+ shortPixels[0] |= (short) (((int) valueR << bitOffsets[1]) & bitMasks[1]);
+ shortPixels[0] |= (short) (((int) valueG << bitOffsets[2]) & bitMasks[2]);
+ if (bitMasks.length > 3) {
+ shortPixels[0] |= (short) (((int) valueB << bitOffsets[3]) & bitMasks[3]);
+ }
+ }
+ //}
+ break;
+ }
+ default:
+ throw new IllegalArgumentException("TransferType not supported: " + dataType);
+
+ }
+
+ dest.setDataElements(x, y, 1, 1, data);
+ }
+ }
+
+ return dest;
+ }
+
+ private static int clamp(final int pValue) {
+ return pValue > 255 ? 255 : pValue;
+ }
+
+ public RenderingHints getRenderingHints() {
+ return null;
+ }
+
+ // TODO: Refactor boilerplate to AbstractBufferedImageOp or use a delegate?
+ // Delegate is maybe better as we won't always implement both BIOp and RasterOP
+ // (but are there ever any time we want to implemnet RasterOp and not BIOp?)
+ public BufferedImage createCompatibleDestImage(BufferedImage src, ColorModel destCM) {
+ ColorModel cm = destCM != null ? destCM : src.getColorModel();
+ return new BufferedImage(cm,
+ ImageUtil.createCompatibleWritableRaster(src, cm, mWidth, mHeight),
+ cm.isAlphaPremultiplied(), null);
+ }
+
+ public WritableRaster createCompatibleDestRaster(Raster src) {
+ return src.createCompatibleWritableRaster(mWidth, mHeight);
+ }
+
+ public Rectangle2D getBounds2D(Raster src) {
+ return new Rectangle(mWidth, mHeight);
+ }
+
+ public Rectangle2D getBounds2D(BufferedImage src) {
+ return new Rectangle(mWidth, mHeight);
+ }
+
+ public Point2D getPoint2D(Point2D srcPt, Point2D dstPt) {
+ // TODO: This is wrong!
+ if (dstPt == null) {
+ if (srcPt instanceof Point2D.Double) {
+ dstPt = new Point2D.Double();
+ }
+ else {
+ dstPt = new Point2D.Float();
+ }
+ }
+ dstPt.setLocation(srcPt);
+
+ return dstPt;
+ }
+
+ public static void main(String[] pArgs) throws IOException {
+ BufferedImage image = ImageIO.read(new File("2006-Lamborghini-Gallardo-Spyder-Y-T-1600x1200.png"));
+ //BufferedImage image = ImageIO.read(new File("focus-rs.jpg"));
+ //BufferedImage image = ImageIO.read(new File("blauesglas_16_bitmask444.bmp"));
+ //image = ImageUtil.toBuffered(image, BufferedImage.TYPE_USHORT_GRAY);
+
+ for (int i = 0; i < 100; i++) {
+ //new PixelizeOp(10).filter(image, null);
+ //new AffineTransformOp(AffineTransform.getScaleInstance(.1, .1), AffineTransformOp.TYPE_NEAREST_NEIGHBOR).filter(image, null);
+ //ImageUtil.toBuffered(image.getScaledInstance(image.getWidth() / 4, image.getHeight() / 4, Image.SCALE_AREA_AVERAGING));
+ //new ResampleOp(image.getWidth() / 10, image.getHeight() / 10, ResampleOp.FILTER_BOX).filter(image, null);
+ //new ResampleOp(image.getWidth() / 10, image.getHeight() / 10, ResampleOp.FILTER_QUADRATIC).filter(image, null);
+ //new AreaAverageOp(image.getWidth() / 10, image.getHeight() / 10).filter(image, null);
+ }
+
+ long start = System.currentTimeMillis();
+ //PixelizeOp pixelizer = new PixelizeOp(image.getWidth() / 10, 1);
+ //pixelizer.setSourceRegion(new Rectangle(0, 2 * image.getHeight() / 3, image.getWidth(), image.getHeight() / 4));
+ //PixelizeOp pixelizer = new PixelizeOp(4);
+ //image = pixelizer.filter(image, image); // Filter in place, that's cool
+ //image = new AffineTransformOp(AffineTransform.getScaleInstance(.25, .25), AffineTransformOp.TYPE_NEAREST_NEIGHBOR).filter(image, null);
+ //image = ImageUtil.toBuffered(image.getScaledInstance(image.getWidth() / 4, image.getHeight() / 4, Image.SCALE_AREA_AVERAGING));
+ //image = new ResampleOp(image.getWidth() / 4, image.getHeight() / 4, ResampleOp.FILTER_BOX).filter(image, null);
+ //image = new ResampleOp(image.getWidth() / 4, image.getHeight() / 4, ResampleOp.FILTER_QUADRATIC).filter(image, null);
+ //image = new AreaAverageOp(image.getWidth() / 7, image.getHeight() / 4).filter(image, null);
+ image = new AreaAverageOp(500, 600).filter(image, null);
+ //image = new ResampleOp(500, 600, ResampleOp.FILTER_BOX).filter(image, null);
+ long time = System.currentTimeMillis() - start;
+
+ System.out.println("time: " + time + " ms");
+
+ JFrame frame = new JFrame("Test");
+ frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+ frame.setContentPane(new JScrollPane(new JLabel(new BufferedImageIcon(image))));
+ frame.pack();
+ frame.setVisible(true);
+ }
+}
diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/BrightnessContrastFilter.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/BrightnessContrastFilter.java
new file mode 100755
index 00000000..97664a38
--- /dev/null
+++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/BrightnessContrastFilter.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (c) 2008, Harald Kuhr
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * Neither the name "TwelveMonkeys" nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.twelvemonkeys.image;
+
+import java.awt.image.RGBImageFilter;
+
+
+/**
+ * Adjusts the contrast and brightness of an image.
+ *
+ * For brightness, the valid range is {@code -2.0,..,0.0,..,2.0}.
+ * A value of {@code 0.0} means no change.
+ * Negative values will make the pixels darker.
+ * Maximum negative value ({@code -2}) will make all filtered pixels black.
+ * Positive values will make the pixels brighter.
+ * Maximum positive value ({@code 2}) will make all filtered pixels white.
+ *
+ * For contrast, the valid range is {@code -1.0,..,0.0,..,1.0}.
+ * A value of {@code 0.0} means no change.
+ * Negative values will reduce contrast.
+ * Maximum negative value ({@code -1}) will make all filtered pixels grey
+ * (no contrast).
+ * Positive values will increase contrast.
+ * Maximum positive value ({@code 1}) will make all filtered pixels primary
+ * colors (either black, white, cyan, magenta, yellow, red, blue or green).
+ *
+ * @author Harald Kuhr
+ * @author last modified by $Author: haku $
+ *
+ * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/BrightnessContrastFilter.java#1 $
+ *
+ * @todo consider doing something similar to http://archives.java.sun.com/cgi-bin/wa?A2=ind0302&L=jai-interest&F=&S=&P=15947
+ */
+
+public class BrightnessContrastFilter extends RGBImageFilter {
+
+ // This filter can filter IndexColorModel, as it is does not depend on
+ // the pixels' location
+ {
+ canFilterIndexColorModel = true;
+ }
+
+ // Use a precalculated lookup table for performace
+ private int[] mLUT = null;
+
+ /**
+ * Creates a BrightnessContrastFilter with default values
+ * ({@code brightness=0.3, contrast=0.3}).
+ *
+ * This will slightly increase both brightness and contrast.
+ */
+ public BrightnessContrastFilter() {
+ this(0.3f, 0.3f);
+ }
+
+ /**
+ * Creates a BrightnessContrastFilter with the given values for brightness
+ * and contrast.
+ *
+ * For brightness, the valid range is {@code -2.0,..,0.0,..,2.0}.
+ * A value of {@code 0.0} means no change.
+ * Negative values will make the pixels darker.
+ * Maximum negative value ({@code -2}) will make all filtered pixels black.
+ * Positive values will make the pixels brighter.
+ * Maximum positive value ({@code 2}) will make all filtered pixels white.
+ *
+ * For contrast, the valid range is {@code -1.0,..,0.0,..,1.0}.
+ * A value of {@code 0.0} means no change.
+ * Negative values will reduce contrast.
+ * Maximum negative value ({@code -1}) will make all filtered pixels grey
+ * (no contrast).
+ * Positive values will increase contrast.
+ * Maximum positive value ({@code 1}) will make all filtered pixels primary
+ * colors (either black, white, cyan, magenta, yellow, red, blue or green).
+ *
+ * @param pBrightness adjust the brightness of the image, in the range
+ * {@code -2.0,..,0.0,..,2.0}.
+ * @param pContrast adjust the contrast of the image, in the range
+ * {@code -1.0,..,0.0,..,1.0}.
+ */
+ public BrightnessContrastFilter(float pBrightness, float pContrast) {
+ mLUT = createLUT(pBrightness, pContrast);
+ }
+
+ private static int[] createLUT(float pBrightness, float pContrast) {
+ int[] lut = new int[256];
+
+ // Hmmm.. This approximates Photoshop values.. Not good though..
+ double contrast = pContrast > 0 ? Math.pow(pContrast, 7.0) * 127.0 : pContrast;
+
+ // Convert range [-1,..,0,..,1] -> [0,..,1,..,2]
+ double brightness = pBrightness + 1.0;
+
+ for (int i = 0; i < 256; i++) {
+ lut[i] = clamp((int) (127.5 * brightness + (i - 127) * (contrast + 1.0)));
+ }
+
+ // Special case, to ensure only primary colors for max contrast
+ if (pContrast == 1f) {
+ lut[127] = lut[126];
+ }
+
+ return lut;
+ }
+
+ private static int clamp(int i) {
+ if (i < 0) {
+ return 0;
+ }
+ if (i > 255) {
+ return 255;
+ }
+ return i;
+ }
+
+ /**
+ * Filters one pixel, adjusting brightness and contrast according to this
+ * filter.
+ *
+ * @param pX x
+ * @param pY y
+ * @param pARGB pixel value in default color space
+ *
+ * @return the filtered pixel value in the default color space
+ */
+
+ public int filterRGB(int pX, int pY, int pARGB) {
+ // Get color components
+ int r = pARGB >> 16 & 0xFF;
+ int g = pARGB >> 8 & 0xFF;
+ int b = pARGB & 0xFF;
+
+ // Scale to new contrast
+ r = mLUT[r];
+ g = mLUT[g];
+ b = mLUT[b];
+
+ // Return ARGB pixel, leave transparency as is
+ return (pARGB & 0xFF000000) | (r << 16) | (g << 8) | b;
+ }
+}
diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/BufferedImageFactory.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/BufferedImageFactory.java
new file mode 100755
index 00000000..06154aef
--- /dev/null
+++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/BufferedImageFactory.java
@@ -0,0 +1,543 @@
+/*
+ * Copyright (c) 2008, Harald Kuhr
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * Neither the name "TwelveMonkeys" nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.twelvemonkeys.image;
+
+import java.awt.*;
+import java.awt.image.*;
+import java.util.*;
+import java.util.List;
+import java.lang.reflect.Array;
+
+/**
+ * A faster, lighter and easier way to convert an {@code Image} to a
+ * {@code BufferedImage} than using a {@code PixelGrabber}.
+ * Clients may provide progress listeners to monitor conversion progress.
+ *
+ * Supports source image subsampling and source region extraction.
+ * Supports source images with 16 bit {@link ColorModel} and
+ * {@link DataBuffer#TYPE_USHORT} transfer type, without converting to
+ * 32 bit/TYPE_INT.
+ *
+ * NOTE: Does not support images with more than one {@code ColorModel} or
+ * different types of pixel data. This is not very common.
+ *
+ * @author Harald Kuhr
+ * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/BufferedImageFactory.java#1 $
+ */
+public final class BufferedImageFactory {
+ private List mListeners;
+ private int mPercentageDone;
+
+ private ImageProducer mProducer;
+ private boolean mError;
+ private boolean mFetching;
+ private boolean mReadColorModelOnly;
+
+ private int mX = 0;
+ private int mY = 0;
+ private int mWidth = -1;
+ private int mHeight = -1;
+
+ private int mXSub = 1;
+ private int mYSub = 1;
+
+ private int mOffset;
+ private int mScanSize;
+
+ private ColorModel mSourceColorModel;
+ private Hashtable mSourceProperties; // ImageConsumer API dictates Hashtable
+
+ private Object mSourcePixels;
+
+ private BufferedImage mBuffered;
+ private ColorModel mColorModel;
+
+ // NOTE: Just to not expose the inheritance
+ private final Consumer mConsumer = new Consumer();
+
+ /**
+ * Creates a {@code BufferedImageFactory}.
+ * @param pSource the source image
+ */
+ public BufferedImageFactory(Image pSource) {
+ this(pSource.getSource());
+ }
+
+ /**
+ * Creates a {@code BufferedImageFactory}.
+ * @param pSource the source image producer
+ */
+ public BufferedImageFactory(ImageProducer pSource) {
+ mProducer = pSource;
+ }
+
+ /**
+ * Returns the {@code BufferedImage} extracted from the given
+ * {@code ImageSource}. Multiple requests will return the same image.
+ *
+ * @return the {@code BufferedImage}
+ *
+ * @throws ImageConversionException if the given {@code ImageSource} cannot
+ * be converted for some reason.
+ */
+ public BufferedImage getBufferedImage() throws ImageConversionException {
+ doFetch(false);
+ return mBuffered;
+ }
+
+ /**
+ * Returns the {@code ColorModel} extracted from the
+ * given {@code ImageSource}. Multiple requests will return the same model.
+ *
+ * @return the {@code ColorModel}
+ *
+ * @throws ImageConversionException if the given {@code ImageSource} cannot
+ * be converted for some reason.
+ */
+ public ColorModel getColorModel() throws ImageConversionException {
+ doFetch(true);
+ return mBuffered != null ? mBuffered.getColorModel() : mColorModel;
+ }
+
+ /**
+ * Frees resources used by this {@code BufferedImageFactory}.
+ */
+ public void dispose() {
+ freeResources();
+ mBuffered = null;
+ mColorModel = null;
+ }
+
+ /**
+ * Aborts the image prodcution.
+ */
+ public void abort() {
+ mConsumer.imageComplete(ImageConsumer.IMAGEABORTED);
+ }
+
+ /**
+ * Sets the source region (AOI) for the new image.
+ *
+ * @param pRect the source region
+ */
+ public void setSourceRegion(Rectangle pRect) {
+ // Refetch everything, if region changed
+ if (mX != pRect.x || mY != pRect.y || mWidth != pRect.width || mHeight != pRect.height) {
+ dispose();
+ }
+
+ mX = pRect.x;
+ mY = pRect.y;
+ mWidth = pRect.width;
+ mHeight = pRect.height;
+ }
+
+ /**
+ * Sets the source subsampling for the new image.
+ *
+ * @param pXSub horisontal subsampling factor
+ * @param pYSub vertical subsampling factor
+ */
+ public void setSourceSubsampling(int pXSub, int pYSub) {
+ // Refetch everything, if subsampling changed
+ if (mXSub != pXSub || mYSub != pYSub) {
+ dispose();
+ }
+
+ if (pXSub > 1) {
+ mXSub = pXSub;
+ }
+ if (pYSub > 1) {
+ mYSub = pYSub;
+ }
+ }
+
+ private synchronized void doFetch(boolean pColorModelOnly) throws ImageConversionException {
+ if (!mFetching && (!pColorModelOnly && mBuffered == null || mBuffered == null && mSourceColorModel == null)) {
+ // NOTE: Subsampling is only applied if extracting full image
+ if (!pColorModelOnly && (mXSub > 1 || mYSub > 1)) {
+ // If only sampling a region, the region must be scaled too
+ if (mWidth > 0 && mHeight > 0) {
+ mWidth = (mWidth + mXSub - 1) / mXSub;
+ mHeight = (mHeight + mYSub - 1) / mYSub;
+
+ mX = (mX + mXSub - 1) / mXSub;
+ mY = (mY + mYSub - 1) / mYSub;
+ }
+
+ mProducer = new FilteredImageSource(mProducer, new SubsamplingFilter(mXSub, mYSub));
+ }
+
+ // Start fetching
+ mFetching = true;
+ mReadColorModelOnly = pColorModelOnly;
+ mProducer.startProduction(mConsumer); // Note: If single-thread (synchronous), this call will block
+
+
+ // Wait until the producer wakes us up, by calling imageComplete
+ while (mFetching) {
+ try {
+ wait();
+ }
+ catch (InterruptedException e) {
+ throw new ImageConversionException("Image conversion aborted: " + e.getMessage(), e);
+ }
+ }
+
+ if (mError) {
+ throw new ImageConversionException("Image conversion failed: ImageConsumer.IMAGEERROR.");
+ }
+
+ if (pColorModelOnly) {
+ createColorModel();
+ }
+ else {
+ createBuffered();
+ }
+ }
+ }
+
+ private void createColorModel() {
+ mColorModel = mSourceColorModel;
+
+ // Clean up, in case any objects are copied/cloned, so we can free resources
+ freeResources();
+ }
+
+ private void createBuffered() {
+ if (mWidth > 0 && mHeight > 0) {
+ if (mSourceColorModel != null && mSourcePixels != null) {
+ // TODO: Fix pixel size / color model problem
+ WritableRaster raster = ImageUtil.createRaster(mWidth, mHeight, mSourcePixels, mSourceColorModel);
+ mBuffered = new BufferedImage(mSourceColorModel, raster, mSourceColorModel.isAlphaPremultiplied(), mSourceProperties);
+ }
+ else {
+ mBuffered = ImageUtil.createClear(mWidth, mHeight, null);
+ }
+ }
+
+ // Clean up, in case any objects are copied/cloned, so we can free resources
+ freeResources();
+ }
+
+ private void freeResources() {
+ mSourceColorModel = null;
+ mSourcePixels = null;
+ mSourceProperties = null;
+ }
+
+ private void processProgress(int mScanline) {
+ if (mListeners != null) {
+ int percent = 100 * mScanline / mHeight;
+
+ //System.out.println("Progress: " + percent + "%");
+
+ if (percent > mPercentageDone) {
+ mPercentageDone = percent;
+
+ // TODO: Fix concurrent modification if a listener removes itself...
+ for (ProgressListener listener : mListeners) {
+ listener.progress(this, percent);
+ }
+ }
+ }
+ }
+
+ /**
+ * Adds a progress listener to this factory.
+ *
+ * @param pListener the progress listener
+ */
+ public void addProgressListener(ProgressListener pListener) {
+ if (mListeners == null) {
+ mListeners = new ArrayList();
+ }
+ mListeners.add(pListener);
+ }
+
+ /**
+ * Removes a progress listener from this factory.
+ *
+ * @param pListener the progress listener
+ */
+ public void removeProgressListener(ProgressListener pListener) {
+ if (mListeners == null) {
+ return;
+ }
+ mListeners.remove(pListener);
+ }
+
+ /**
+ * Removes all progress listeners from this factory.
+ */
+ public void removeAllProgressListeners() {
+ if (mListeners != null) {
+ mListeners.clear();
+ }
+ }
+
+ /**
+ * Converts an array of {@code int} pixles to an array of {@code short}
+ * pixels. The conversion is done, by masking out the
+ * higher 16 bits of the {@code int}.
+ *
+ * For eny given {@code int}, the {@code short} value is computed as
+ * follows:
+ *
{@code
+ * short value = (short) (intValue & 0x0000ffff);
+ * }
+ *
+ * @param pPixels the pixel data to convert
+ * @return an array of {@code short}s, same lenght as {@code pPixels}
+ */
+ private static short[] toShortPixels(int[] pPixels) {
+ short[] pixels = new short[pPixels.length];
+ for (int i = 0; i < pixels.length; i++) {
+ pixels[i] = (short) (pPixels[i] & 0xffff);
+ }
+ return pixels;
+ }
+
+ /**
+ * This interface allows clients of a {@code BufferedImageFactory} to
+ * receive notifications of decoding progress.
+ *
+ * @see BufferedImageFactory#addProgressListener
+ * @see BufferedImageFactory#removeProgressListener
+ */
+ public static interface ProgressListener extends EventListener {
+
+ /**
+ * Reports progress to this listener.
+ * Invoked by the {@code BufferedImageFactory} to report progress in
+ * the image decoding.
+ *
+ * @param pFactory the factory reporting the progress
+ * @param pPercentage the perccentage of progress
+ */
+ void progress(BufferedImageFactory pFactory, float pPercentage);
+ }
+
+ private class Consumer implements ImageConsumer {
+ /**
+ * Implementation of all setPixels methods.
+ * Note that this implementation assumes that all invocations for one
+ * image uses the same color model, and that the pixel data has the
+ * same type.
+ *
+ * @param pX x coordinate of pixel data region
+ * @param pY y coordinate of pixel data region
+ * @param pWidth width of pixel data region
+ * @param pHeight height of pixel data region
+ * @param pModel the color model of the pixel data
+ * @param pPixels the pixel data array
+ * @param pOffset the offset into the pixel data array
+ * @param pScanSize the scan size of the pixel data array
+ */
+ private void setPixelsImpl(int pX, int pY, int pWidth, int pHeight, ColorModel pModel, Object pPixels, int pOffset, int pScanSize) {
+ setColorModelOnce(pModel);
+
+ if (pPixels == null) {
+ return;
+ }
+
+ //System.out.println("Setting " + pPixels.getClass().getComponentType() + " pixels: " + Array.getLength(pPixels));
+
+ // Allocate array if neccessary
+ if (mSourcePixels == null) {
+ /*
+ System.out.println("ColorModel: " + pModel);
+ System.out.println("Scansize: " + pScanSize + " TrasferType: " + ImageUtil.getTransferType(pModel));
+ System.out.println("Creating " + pPixels.getClass().getComponentType() + " array of length " + (mWidth * mHeight));
+ */
+ // Allocate a suitable source pixel array
+ // TODO: Should take pixel "width" into consideration, for byte packed rasters?!
+ // OR... Is anything but single-pixel models really supported by the API?
+ mSourcePixels = Array.newInstance(pPixels.getClass().getComponentType(), mWidth * mHeight);
+ mScanSize = mWidth;
+ mOffset = 0;
+ }
+ else if (mSourcePixels.getClass() != pPixels.getClass()) {
+ throw new IllegalStateException("Only one pixel type allowed");
+ }
+
+ // AOI stuff
+ if (pY < mY) {
+ int diff = mY - pY;
+ if (diff >= pHeight) {
+ return;
+ }
+ pOffset += pScanSize * diff;
+ pY += diff;
+ pHeight -= diff;
+ }
+ if (pY + pHeight > mY + mHeight) {
+ pHeight = (mY + mHeight) - pY;
+ if (pHeight <= 0) {
+ return;
+ }
+ }
+
+ if (pX < mX) {
+ int diff = mX - pX;
+ if (diff >= pWidth) {
+ return;
+ }
+ pOffset += diff;
+ pX += diff;
+ pWidth -= diff;
+ }
+ if (pX + pWidth > mX + mWidth) {
+ pWidth = (mX + mWidth) - pX;
+ if (pWidth <= 0) {
+ return;
+ }
+ }
+
+ int dstOffset = mOffset + (pY - mY) * mScanSize + (pX - mX);
+
+ // Do the pixel copying
+ for (int i = pHeight; i > 0; i--) {
+ System.arraycopy(pPixels, pOffset, mSourcePixels, dstOffset, pWidth);
+ pOffset += pScanSize;
+ dstOffset += mScanSize;
+ }
+
+ processProgress(pY + pHeight);
+ }
+
+ /** {@code ImageConsumer} implementation, do not invoke directly */
+ public void setPixels(int pX, int pY, int pWidth, int pHeight, ColorModel pModel, short[] pPixels, int pOffset, int pScanSize) {
+ setPixelsImpl(pX, pY, pWidth, pHeight, pModel, pPixels, pOffset, pScanSize);
+ }
+
+ private void setColorModelOnce(ColorModel pModel) {
+ // NOTE: There seems to be a "bug" in AreaAveragingScaleFilter, as it
+ // first passes the original colormodel through in setColorModel, then
+ // later replaces it with the default RGB in the first setPixels call
+ // (this is probably allowed according to the spec, but it's a waste of
+ // time and space).
+ if (mSourceColorModel != pModel) {
+ if (/*mSourceColorModel == null ||*/ mSourcePixels == null) {
+ mSourceColorModel = pModel;
+ }
+ else {
+ throw new IllegalStateException("Change of ColorModel after pixel delivery not supported");
+ }
+ }
+
+ // If color model is all we ask for, stop now
+ if (mReadColorModelOnly) {
+ mConsumer.imageComplete(ImageConsumer.IMAGEABORTED);
+ }
+ }
+
+ /** {@code ImageConsumer} implementation, do not invoke */
+ public void imageComplete(int pStatus) {
+ mFetching = false;
+
+ if (mProducer != null) {
+ mProducer.removeConsumer(this);
+ }
+
+ switch (pStatus) {
+ case IMAGEERROR:
+ new Error().printStackTrace();
+ mError = true;
+ break;
+ }
+
+ synchronized (BufferedImageFactory.this) {
+ BufferedImageFactory.this.notifyAll();
+ }
+ }
+
+ /** {@code ImageConsumer} implementation, do not invoke directly */
+ public void setColorModel(ColorModel pModel) {
+ //System.out.println("SetColorModel: " + pModel);
+ setColorModelOnce(pModel);
+ }
+
+ /** {@code ImageConsumer} implementation, do not invoke directly */
+ public void setDimensions(int pWidth, int pHeight) {
+ //System.out.println("Setting dimensions: " + pWidth + ", " + pHeight);
+ if (mWidth < 0) {
+ mWidth = pWidth - mX;
+ }
+ if (mHeight < 0) {
+ mHeight = pHeight - mY;
+ }
+
+ // Hmm.. Special case, but is it a good idea?
+ if (mWidth <= 0 || mHeight <= 0) {
+ imageComplete(STATICIMAGEDONE);
+ }
+ }
+
+ /** {@code ImageConsumer} implementation, do not invoke directly */
+ public void setHints(int pHintflags) {
+ // ignore
+ }
+
+ /** {@code ImageConsumer} implementation, do not invoke directly */
+ public void setPixels(int pX, int pY, int pWidth, int pHeight, ColorModel pModel, byte[] pPixels, int pOffset, int pScanSize) {
+ /*if (pModel.getPixelSize() < 8) {
+ // Byte packed
+ setPixelsImpl(pX, pY, pWidth, pHeight, pModel, toBytePackedPixels(pPixels, pModel.getPixelSize()), pOffset, pScanSize);
+ }
+ /*
+ else if (pModel.getPixelSize() > 8) {
+ // Byte interleaved
+ setPixelsImpl(pX, pY, pWidth, pHeight, pModel, toByteInterleavedPixels(pPixels), pOffset, pScanSize);
+ }
+ */
+ //else {
+ // Default, pixelSize == 8, one byte pr pixel
+ setPixelsImpl(pX, pY, pWidth, pHeight, pModel, pPixels, pOffset, pScanSize);
+ //}
+ }
+
+ /** {@code ImageConsumer} implementation, do not invoke directly */
+ public void setPixels(int pX, int pY, int pWeigth, int pHeight, ColorModel pModel, int[] pPixels, int pOffset, int pScanSize) {
+ if (ImageUtil.getTransferType(pModel) == DataBuffer.TYPE_USHORT) {
+ // NOTE: Workaround for limitation in ImageConsumer API
+ // Convert int[] to short[], to be compatible with the ColorModel
+ setPixelsImpl(pX, pY, pWeigth, pHeight, pModel, toShortPixels(pPixels), pOffset, pScanSize);
+ }
+ else {
+ setPixelsImpl(pX, pY, pWeigth, pHeight, pModel, pPixels, pOffset, pScanSize);
+ }
+ }
+
+ /** {@code ImageConsumer} implementation, do not invoke directly */
+ public void setProperties(Hashtable pProperties) {
+ mSourceProperties = pProperties;
+ }
+ }
+}
\ No newline at end of file
diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/BufferedImageIcon.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/BufferedImageIcon.java
new file mode 100755
index 00000000..d254cdff
--- /dev/null
+++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/BufferedImageIcon.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (c) 2008, Harald Kuhr
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * Neither the name "TwelveMonkeys" nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.twelvemonkeys.image;
+
+import javax.swing.Icon;
+import java.awt.image.BufferedImage;
+import java.awt.*;
+import java.awt.geom.AffineTransform;
+
+/**
+ * An {@code Icon} implementation backed by a {@code BufferedImage}.
+ *
+ *
+ * @author Harald Kuhr
+ * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/BufferedImageIcon.java#2 $
+ */
+public class BufferedImageIcon implements Icon {
+ private final BufferedImage mImage;
+ private int mWidth;
+ private int mHeight;
+ private final boolean mFast;
+
+ public BufferedImageIcon(BufferedImage pImage) {
+ this(pImage, pImage.getWidth(), pImage.getHeight());
+ }
+
+ public BufferedImageIcon(BufferedImage pImage, int pWidth, int pHeight) {
+ if (pImage == null) {
+ throw new IllegalArgumentException("image == null");
+ }
+ if (pWidth <= 0 || pHeight <= 0) {
+ throw new IllegalArgumentException("Icon size must be positive");
+ }
+
+ mImage = pImage;
+ mWidth = pWidth;
+ mHeight = pHeight;
+
+ mFast = pImage.getWidth() == mWidth && pImage.getHeight() == mHeight;
+ }
+
+ public int getIconHeight() {
+ return mHeight;
+ }
+
+ public int getIconWidth() {
+ return mWidth;
+ }
+
+ public void paintIcon(Component c, Graphics g, int x, int y) {
+ if (mFast || !(g instanceof Graphics2D)) {
+ //System.out.println("Scaling fast");
+ g.drawImage(mImage, x, y, mWidth, mHeight, null);
+ }
+ else {
+ //System.out.println("Scaling using interpolation");
+ Graphics2D g2 = (Graphics2D) g;
+ AffineTransform xform = AffineTransform.getTranslateInstance(x, y);
+ xform.scale(mWidth / (double) mImage.getWidth(), mHeight / (double) mImage.getHeight());
+ g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
+ RenderingHints.VALUE_INTERPOLATION_BILINEAR);
+ g2.drawImage(mImage, xform, null);
+ }
+ }
+}
diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ConvolveTester.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ConvolveTester.java
new file mode 100755
index 00000000..437c6449
--- /dev/null
+++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ConvolveTester.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (c) 2008, Harald Kuhr
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * Neither the name "TwelveMonkeys" nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.twelvemonkeys.image;
+
+import javax.imageio.ImageIO;
+import javax.swing.*;
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.awt.image.Kernel;
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+
+/**
+ * ConvolveTester
+ *
+ * @author Harald Kuhr
+ * @author last modified by $Author: haku $
+ * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ConvolveTester.java#1 $
+ */
+public class ConvolveTester {
+
+ // Initial sample timings (avg, 1000 iterations)
+ // PNG, type 0: JPEG, type 3:
+ // ZERO_FILL: 5.4 ms 4.6 ms
+ // NO_OP: 5.4 ms 4.6 ms
+ // REFLECT: 42.4 ms 24.9 ms
+ // WRAP: 86.9 ms 29.5 ms
+
+ final static int ITERATIONS = 1000;
+
+ public static void main(String[] pArgs) throws IOException {
+ File input = new File(pArgs[0]);
+ BufferedImage image = ImageIO.read(input);
+ BufferedImage result = null;
+
+ System.out.println("image: " + image);
+
+ if (pArgs.length > 1) {
+ float ammount = Float.parseFloat(pArgs[1]);
+
+ int edgeOp = pArgs.length > 2 ? Integer.parseInt(pArgs[2]) : ImageUtil.EDGE_REFLECT;
+
+ long start = System.currentTimeMillis();
+ for (int i = 0; i < ITERATIONS; i++) {
+ result = sharpen(image, ammount, edgeOp);
+ }
+ long end = System.currentTimeMillis();
+ System.out.println("Time: " + ((end - start) / (double) ITERATIONS) + "ms");
+
+ showIt(result, "Sharpened " + ammount + " " + input.getName());
+ }
+ else {
+ showIt(image, "Original " + input.getName());
+ }
+
+ }
+
+ public static void showIt(final BufferedImage pImage, final String pTitle) {
+ try {
+ SwingUtilities.invokeAndWait(new Runnable() {
+ public void run() {
+ JFrame frame = new JFrame(pTitle);
+ frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+ frame.setLocationByPlatform(true);
+ JPanel pane = new JPanel(new BorderLayout());
+ GraphicsConfiguration gc = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration();
+ BufferedImageIcon icon = new BufferedImageIcon(ImageUtil.accelerate(pImage, gc));
+ JScrollPane scroll = new JScrollPane(new JLabel(icon));
+ scroll.setBorder(null);
+ pane.add(scroll);
+ frame.setContentPane(pane);
+ frame.pack();
+ frame.setVisible(true);
+ }
+ });
+ }
+ catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ catch (InvocationTargetException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ static BufferedImage sharpen(BufferedImage pOriginal, final float pAmmount, int pEdgeOp) {
+ if (pAmmount == 0f) {
+ return pOriginal;
+ }
+
+ // Create the convolution matrix
+ float[] data = new float[]{
+ 0.0f, -pAmmount, 0.0f,
+ -pAmmount, 4f * pAmmount + 1f, -pAmmount,
+ 0.0f, -pAmmount, 0.0f
+ };
+
+ // Do the filtering
+ return ImageUtil.convolve(pOriginal, new Kernel(3, 3, data), pEdgeOp);
+
+ }
+}
diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ConvolveWithEdgeOp.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ConvolveWithEdgeOp.java
new file mode 100755
index 00000000..0e377f6c
--- /dev/null
+++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ConvolveWithEdgeOp.java
@@ -0,0 +1,244 @@
+/*
+ * Copyright (c) 2008, Harald Kuhr
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * Neither the name "TwelveMonkeys" nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.twelvemonkeys.image;
+
+import java.awt.image.*;
+import java.awt.*;
+import java.awt.geom.Rectangle2D;
+import java.awt.geom.Point2D;
+
+/**
+ * This class implements a convolution from the source
+ * to the destination.
+ *
+ * @author Harald Kuhr
+ * @author last modified by $Author: haku $
+ * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ConvolveWithEdgeOp.java#1 $
+ *
+ * @see java.awt.image.ConvolveOp
+ */
+public class ConvolveWithEdgeOp implements BufferedImageOp, RasterOp {
+
+ /**
+ * Alias for {@link ConvolveOp#EDGE_ZERO_FILL}.
+ * @see #EDGE_REFLECT
+ */
+ public static final int EDGE_ZERO_FILL = ConvolveOp.EDGE_ZERO_FILL;
+ /**
+ * Alias for {@link ConvolveOp#EDGE_NO_OP}.
+ * @see #EDGE_REFLECT
+ */
+ public static final int EDGE_NO_OP = ConvolveOp.EDGE_NO_OP;
+ /**
+ * Adds a border to the image while convolving. The border will reflect the
+ * edges of the original image. This is usually a good default.
+ * Note that while this mode typically provides better quality than the
+ * standard modes {@code EDGE_ZERO_FILL} and {@code EDGE_NO_OP}, it does so
+ * at the expense of higher memory consumption and considerable more computation.
+ */
+ public static final int EDGE_REFLECT = 2; // as JAI BORDER_REFLECT
+ /**
+ * Adds a border to the image while convolving. The border will wrap the
+ * edges of the original image. This is usually the best choice for tiles.
+ * Note that while this mode typically provides better quality than the
+ * standard modes {@code EDGE_ZERO_FILL} and {@code EDGE_NO_OP}, it does so
+ * at the expense of higher memory consumption and considerable more computation.
+ * @see #EDGE_REFLECT
+ */
+ public static final int EDGE_WRAP = 3; // as JAI BORDER_WRAP
+
+ private final Kernel mKernel;
+ private final int mEdgeCondition;
+
+ private final ConvolveOp mConvolve;
+
+ public ConvolveWithEdgeOp(final Kernel pKernel, final int pEdgeCondition, final RenderingHints pHints) {
+ // Create convolution operation
+ int edge;
+ switch (pEdgeCondition) {
+ case EDGE_REFLECT:
+ case EDGE_WRAP:
+ edge = ConvolveOp.EDGE_NO_OP;
+ break;
+ default:
+ edge = pEdgeCondition;
+ break;
+ }
+ mKernel = pKernel;
+ mEdgeCondition = pEdgeCondition;
+ mConvolve = new ConvolveOp(pKernel, edge, pHints);
+ }
+
+ public ConvolveWithEdgeOp(final Kernel pKernel) {
+ this(pKernel, EDGE_ZERO_FILL, null);
+ }
+
+ public BufferedImage filter(BufferedImage pSource, BufferedImage pDestination) {
+ if (pSource == null) {
+ throw new NullPointerException("source image is null");
+ }
+ if (pSource == pDestination) {
+ throw new IllegalArgumentException("source image cannot be the same as the destination image");
+ }
+
+ int borderX = mKernel.getWidth() / 2;
+ int borderY = mKernel.getHeight() / 2;
+
+ BufferedImage original = addBorder(pSource, borderX, borderY);
+
+ // Workaround for what seems to be a Java2D bug:
+ // ConvolveOp needs explicit destination image type for some "uncommon"
+ // image types. However, TYPE_3BYTE_BGR is what javax.imageio.ImageIO
+ // normally returns for color JPEGs... :-/
+ BufferedImage destination = pDestination;
+ if (original.getType() == BufferedImage.TYPE_3BYTE_BGR) {
+ destination = ImageUtil.createBuffered(
+ pSource.getWidth(), pSource.getHeight(),
+ pSource.getType(), pSource.getColorModel().getTransparency(),
+ null
+ );
+ }
+
+ // Do the filtering (if destination is null, a new image will be created)
+ destination = mConvolve.filter(original, destination);
+
+ if (pSource != original) {
+ // Remove the border
+ destination = destination.getSubimage(borderX, borderY, pSource.getWidth(), pSource.getHeight());
+ }
+
+ return destination;
+ }
+
+ private BufferedImage addBorder(final BufferedImage pOriginal, final int pBorderX, final int pBorderY) {
+ if ((mEdgeCondition & 2) == 0) {
+ return pOriginal;
+ }
+
+ // TODO: Might be faster if we could clone raster and strech it...
+ int w = pOriginal.getWidth();
+ int h = pOriginal.getHeight();
+
+ ColorModel cm = pOriginal.getColorModel();
+ WritableRaster raster = cm.createCompatibleWritableRaster(w + 2 * pBorderX, h + 2 * pBorderY);
+ BufferedImage bordered = new BufferedImage(cm, raster, cm.isAlphaPremultiplied(), null);
+
+ Graphics2D g = bordered.createGraphics();
+ try {
+ g.setComposite(AlphaComposite.Src);
+ g.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_DISABLE);
+
+ // Draw original in center
+ g.drawImage(pOriginal, pBorderX, pBorderY, null);
+
+ // TODO: I guess we need the top/left etc, if the corner pixels are covered by the kernel
+ switch (mEdgeCondition) {
+ case EDGE_REFLECT:
+ // Top/left (empty)
+ g.drawImage(pOriginal, pBorderX, 0, pBorderX + w, pBorderY, 0, 0, w, 1, null); // Top/center
+ // Top/right (empty)
+
+ g.drawImage(pOriginal, -w + pBorderX, pBorderY, pBorderX, h + pBorderY, 0, 0, 1, h, null); // Center/left
+ // Center/center (already drawn)
+ g.drawImage(pOriginal, w + pBorderX, pBorderY, 2 * pBorderX + w, h + pBorderY, w - 1, 0, w, h, null); // Center/right
+
+ // Bottom/left (empty)
+ g.drawImage(pOriginal, pBorderX, pBorderY + h, pBorderX + w, 2 * pBorderY + h, 0, h - 1, w, h, null); // Bottom/center
+ // Bottom/right (empty)
+ break;
+ case EDGE_WRAP:
+ g.drawImage(pOriginal, -w + pBorderX, -h + pBorderY, null); // Top/left
+ g.drawImage(pOriginal, pBorderX, -h + pBorderY, null); // Top/center
+ g.drawImage(pOriginal, w + pBorderX, -h + pBorderY, null); // Top/right
+
+ g.drawImage(pOriginal, -w + pBorderX, pBorderY, null); // Center/left
+ // Center/center (already drawn)
+ g.drawImage(pOriginal, w + pBorderX, pBorderY, null); // Center/right
+
+ g.drawImage(pOriginal, -w + pBorderX, h + pBorderY, null); // Bottom/left
+ g.drawImage(pOriginal, pBorderX, h + pBorderY, null); // Bottom/center
+ g.drawImage(pOriginal, w + pBorderX, h + pBorderY, null); // Bottom/right
+ break;
+ default:
+ throw new IllegalArgumentException("Illegal edge operation " + mEdgeCondition);
+ }
+
+ }
+ finally {
+ g.dispose();
+ }
+
+ return bordered;
+ }
+
+ /**
+ * Returns the edge condition.
+ * @return the edge condition of this {@code ConvolveOp}.
+ * @see #EDGE_NO_OP
+ * @see #EDGE_ZERO_FILL
+ * @see #EDGE_REFLECT
+ * @see #EDGE_WRAP
+ */
+ public int getEdgeCondition() {
+ return mEdgeCondition;
+ }
+
+ public WritableRaster filter(final Raster pSource, final WritableRaster pDestination) {
+ return mConvolve.filter(pSource, pDestination);
+ }
+
+ public BufferedImage createCompatibleDestImage(final BufferedImage pSource, final ColorModel pDesinationColorModel) {
+ return mConvolve.createCompatibleDestImage(pSource, pDesinationColorModel);
+ }
+
+ public WritableRaster createCompatibleDestRaster(final Raster pSource) {
+ return mConvolve.createCompatibleDestRaster(pSource);
+ }
+
+ public Rectangle2D getBounds2D(final BufferedImage pSource) {
+ return mConvolve.getBounds2D(pSource);
+ }
+
+ public Rectangle2D getBounds2D(final Raster pSource) {
+ return mConvolve.getBounds2D(pSource);
+ }
+
+ public Point2D getPoint2D(final Point2D pSourcePoint, final Point2D pDestinationPoint) {
+ return mConvolve.getPoint2D(pSourcePoint, pDestinationPoint);
+ }
+
+ public RenderingHints getRenderingHints() {
+ return mConvolve.getRenderingHints();
+ }
+
+ public Kernel getKernel() {
+ return mConvolve.getKernel();
+ }
+
+}
diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/CopyDither.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/CopyDither.java
new file mode 100755
index 00000000..0e4b3e6e
--- /dev/null
+++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/CopyDither.java
@@ -0,0 +1,298 @@
+/*
+ * Copyright (c) 2008, Harald Kuhr
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * Neither the name "TwelveMonkeys" nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.twelvemonkeys.image;
+
+import java.awt.*;
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+import java.awt.image.BufferedImage;
+import java.awt.image.BufferedImageOp;
+import java.awt.image.ColorModel;
+import java.awt.image.IndexColorModel;
+import java.awt.image.Raster;
+import java.awt.image.RasterOp;
+import java.awt.image.WritableRaster;
+
+/**
+ * This BufferedImageOp simply copies pixels, converting to a
+ * {@code IndexColorModel}.
+
+ * @author Harald Kuhr
+ * @author last modified by $Author: haku $
+ *
+ * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/CopyDither.java#1 $
+ *
+ */
+public class CopyDither implements BufferedImageOp, RasterOp {
+
+ protected IndexColorModel mIndexColorModel = null;
+
+ /**
+ * Creates a {@code CopyDither}, using the given
+ * {@code IndexColorModel} for dithering into.
+ *
+ * @param pICM an IndexColorModel.
+ */
+ public CopyDither(IndexColorModel pICM) {
+ // Store colormodel
+ mIndexColorModel = pICM;
+ }
+
+ /**
+ * Creates a {@code CopyDither}, with no fixed
+ * {@code IndexColorModel}. The colormodel will be generated for each
+ * filtering, unless the dest image allready has an
+ * {@code IndexColorModel}.
+ */
+ public CopyDither() {
+ }
+
+
+ /**
+ * Creates a compatible {@code BufferedImage} to dither into.
+ * Only {@code IndexColorModel} allowed.
+ *
+ * @return a compatible {@code BufferedImage}
+ *
+ * @throws ImageFilterException if {@code pDestCM} is not {@code null} or
+ * an instance of {@code IndexColorModel}.
+ */
+ public final BufferedImage createCompatibleDestImage(BufferedImage pSource,
+ ColorModel pDestCM) {
+ if (pDestCM == null) {
+ return new BufferedImage(pSource.getWidth(), pSource.getHeight(),
+ BufferedImage.TYPE_BYTE_INDEXED,
+ mIndexColorModel);
+ }
+ else if (pDestCM instanceof IndexColorModel) {
+ return new BufferedImage(pSource.getWidth(), pSource.getHeight(),
+ BufferedImage.TYPE_BYTE_INDEXED,
+ (IndexColorModel) pDestCM);
+ }
+ else {
+ throw new ImageFilterException("Only IndexColorModel allowed.");
+ }
+ }
+
+ /**
+ * Creates a compatible {@code Raster} to dither into.
+ * Only {@code IndexColorModel} allowed.
+ *
+ * @param pSrc
+ *
+ * @return a {@code WritableRaster}
+ */
+ public final WritableRaster createCompatibleDestRaster(Raster pSrc) {
+ return createCompatibleDestRaster(pSrc, getICM(pSrc));
+ }
+
+ public final WritableRaster createCompatibleDestRaster(Raster pSrc,
+ IndexColorModel pIndexColorModel) {
+ /*
+ return new BufferedImage(pSrc.getWidth(), pSrc.getHeight(),
+ BufferedImage.TYPE_BYTE_INDEXED,
+ pIndexColorModel).getRaster();
+ */
+ return pIndexColorModel.createCompatibleWritableRaster(pSrc.getWidth(), pSrc.getHeight());
+ }
+
+
+ /**
+ * Returns the bounding box of the filtered destination image. Since
+ * this is not a geometric operation, the bounding box does not
+ * change.
+ * @param pSrc the {@code BufferedImage} to be filtered
+ * @return the bounds of the filtered definition image.
+ */
+ public final Rectangle2D getBounds2D(BufferedImage pSrc) {
+ return getBounds2D(pSrc.getRaster());
+ }
+
+ /**
+ * Returns the bounding box of the filtered destination Raster. Since
+ * this is not a geometric operation, the bounding box does not
+ * change.
+ * @param pSrc the {@code Raster} to be filtered
+ * @return the bounds of the filtered definition {@code Raster}.
+ */
+ public final Rectangle2D getBounds2D(Raster pSrc) {
+ return pSrc.getBounds();
+
+ }
+
+ /**
+ * Returns the location of the destination point given a
+ * point in the source. If {@code dstPt} is not
+ * {@code null}, it will be used to hold the return value.
+ * Since this is not a geometric operation, the {@code srcPt}
+ * will equal the {@code dstPt}.
+ * @param pSrcPt a {@code Point2D} that represents a point
+ * in the source image
+ * @param pDstPt a {@code Point2D}that represents the location
+ * in the destination
+ * @return the {@code Point2D} in the destination that
+ * corresponds to the specified point in the source.
+ */
+ public final Point2D getPoint2D(Point2D pSrcPt, Point2D pDstPt) {
+ // Create new Point, if needed
+ if (pDstPt == null) {
+ pDstPt = new Point2D.Float();
+ }
+
+ // Copy location
+ pDstPt.setLocation(pSrcPt.getX(), pSrcPt.getY());
+
+ // Return dest
+ return pDstPt;
+ }
+
+ /**
+ * Returns the rendering mHints for this op.
+ * @return the {@code RenderingHints} object associated
+ * with this op.
+ */
+ public final RenderingHints getRenderingHints() {
+ return null;
+ }
+
+ /**
+ * Converts a int triplet to int ARGB.
+ */
+ private static int toIntARGB(int[] pRGB) {
+ return 0xff000000 // All opaque
+ | (pRGB[0] << 16)
+ | (pRGB[1] << 8)
+ | (pRGB[2]);
+ /*
+ | ((int) (pRGB[0] << 16) & 0x00ff0000)
+ | ((int) (pRGB[1] << 8) & 0x0000ff00)
+ | ((int) (pRGB[2] ) & 0x000000ff);
+ */
+ }
+
+
+ /**
+ * Performs a single-input/single-output dither operation, applying basic
+ * Floyd-Steinberg error-diffusion to the image.
+ *
+ * @param pSource the source image
+ * @param pDest the destiantion image
+ *
+ * @return the destination image, or a new image, if {@code pDest} was
+ * {@code null}.
+ */
+ public final BufferedImage filter(BufferedImage pSource,
+ BufferedImage pDest) {
+ // Create destination image, if none provided
+ if (pDest == null) {
+ pDest = createCompatibleDestImage(pSource, getICM(pSource));
+ }
+ else if (!(pDest.getColorModel() instanceof IndexColorModel)) {
+ throw new ImageFilterException("Only IndexColorModel allowed.");
+ }
+
+ // Filter rasters
+ filter(pSource.getRaster(), pDest.getRaster(), (IndexColorModel) pDest.getColorModel());
+
+ return pDest;
+ }
+
+ /**
+ * Performs a single-input/single-output dither operation, applying basic
+ * Floyd-Steinberg error-diffusion to the image.
+ *
+ * @param pSource
+ * @param pDest
+ *
+ * @return the destination raster, or a new raster, if {@code pDest} was
+ * {@code null}.
+ */
+ public final WritableRaster filter(final Raster pSource, WritableRaster pDest) {
+ return filter(pSource, pDest, getICM(pSource));
+ }
+
+ private IndexColorModel getICM(BufferedImage pSource) {
+ return (mIndexColorModel != null ? mIndexColorModel : IndexImage.getIndexColorModel(pSource, 256, IndexImage.TRANSPARENCY_BITMASK | IndexImage.COLOR_SELECTION_QUALITY));
+ }
+ private IndexColorModel getICM(Raster pSource) {
+ return (mIndexColorModel != null ? mIndexColorModel : createIndexColorModel(pSource));
+ }
+
+ private IndexColorModel createIndexColorModel(Raster pSource) {
+ BufferedImage image = new BufferedImage(pSource.getWidth(), pSource.getHeight(),
+ BufferedImage.TYPE_INT_ARGB);
+ image.setData(pSource);
+ return IndexImage.getIndexColorModel(image, 256, IndexImage.TRANSPARENCY_BITMASK | IndexImage.COLOR_SELECTION_QUALITY);
+ }
+
+ /**
+ * Performs a single-input/single-output pixel copy operation.
+ *
+ * @param pSource
+ * @param pDest
+ * @param pColorModel
+ *
+ * @return the destination raster, or a new raster, if {@code pDest} was
+ * {@code null}.
+ */
+ public final WritableRaster filter(final Raster pSource, WritableRaster pDest,
+ IndexColorModel pColorModel) {
+ int width = pSource.getWidth();
+ int height = pSource.getHeight();
+
+ if (pDest == null) {
+ pDest = createCompatibleDestRaster(pSource, pColorModel);
+ }
+
+ // temp buffers
+ final int[] inRGB = new int[4];
+ Object pixel = null;
+
+ // TODO: Use getPixels instead of getPixel for better performance?
+
+ // Loop through image data
+ for (int y = 0; y < height; y++) {
+ for (int x = 0; x < width; x++) {
+ // Get rgb from original raster
+ // DON'T KNOW IF THIS WILL WORK FOR ALL TYPES..?
+ pSource.getPixel(x, y, inRGB);
+
+ // Get pixel value...
+ // It is VERY important that we are using an IndexColorModel that
+ // support reverse color lookup for speed.
+ pixel = pColorModel.getDataElements(toIntARGB(inRGB), pixel);
+
+ // And set it
+ pDest.setDataElements(x, y, pixel);
+ }
+ }
+ return pDest;
+ }
+}
+
diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/DiffusionDither.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/DiffusionDither.java
new file mode 100755
index 00000000..5d9c4e31
--- /dev/null
+++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/DiffusionDither.java
@@ -0,0 +1,465 @@
+package com.twelvemonkeys.image;
+
+
+import java.awt.*;
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+import java.awt.image.BufferedImage;
+import java.awt.image.BufferedImageOp;
+import java.awt.image.ColorModel;
+import java.awt.image.IndexColorModel;
+import java.awt.image.Raster;
+import java.awt.image.RasterOp;
+import java.awt.image.WritableRaster;
+import java.util.Random;
+
+/**
+ * This {@code BufferedImageOp/RasterOp} implements basic
+ * Floyd-Steinberg error-diffusion algorithm for dithering.
+ *
+ * The weights used are 7/16 3/16 5/16 1/16, distributed like this:
+ *
+ *
+ *
+ *
X
7/16
+ *
3/16
5/16
1/16
+ *
+ *
+ * See Computer Graphics (Foley et al.)
+ * for more information.
+ *
+ * @author Harald Kuhr
+ * @author last modified by $Author: haku $
+ *
+ * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/DiffusionDither.java#1 $
+ *
+ */
+public class DiffusionDither implements BufferedImageOp, RasterOp {
+
+ protected IndexColorModel mIndexColorModel = null;
+ private boolean mAlternateScans = true;
+ private static final int FS_SCALE = 1 << 8;
+ private static final Random RANDOM = new Random();
+
+ /**
+ * Creates a {@code DiffusionDither}, using the given
+ * {@code IndexColorModel} for dithering into.
+ *
+ * @param pICM an IndexColorModel.
+ */
+ public DiffusionDither(IndexColorModel pICM) {
+ // Store colormodel
+ mIndexColorModel = pICM;
+ }
+
+ /**
+ * Creates a {@code DiffusionDither}, with no fixed
+ * {@code IndexColorModel}. The colormodel will be generated for each
+ * filtering, unless the dest image allready has an
+ * {@code IndexColorModel}.
+ */
+ public DiffusionDither() {
+ }
+
+ /**
+ * Sets the scan mode. If the parameter is true, error distribution for
+ * every even line will be left-to-right, while odd lines will be
+ * right-to-left.
+ *
+ * @param pUse {@code true} if scan mode should be alternating left/right
+ */
+ public void setAlternateScans(boolean pUse) {
+ mAlternateScans = pUse;
+ }
+
+ /**
+ * Creates a compatible {@code BufferedImage} to dither into.
+ * Only {@code IndexColorModel} allowed.
+ *
+ * @return a compatible {@code BufferedImage}
+ *
+ * @throws ImageFilterException if {@code pDestCM} is not {@code null} or
+ * an instance of {@code IndexColorModel}.
+ */
+ public final BufferedImage createCompatibleDestImage(BufferedImage pSource,
+ ColorModel pDestCM) {
+ if (pDestCM == null) {
+ return new BufferedImage(pSource.getWidth(), pSource.getHeight(),
+ BufferedImage.TYPE_BYTE_INDEXED,
+ getICM(pSource));
+ }
+ else if (pDestCM instanceof IndexColorModel) {
+ return new BufferedImage(pSource.getWidth(), pSource.getHeight(),
+ BufferedImage.TYPE_BYTE_INDEXED,
+ (IndexColorModel) pDestCM);
+ }
+ else {
+ throw new ImageFilterException("Only IndexColorModel allowed.");
+ }
+ }
+
+ /**
+ * Creates a compatible {@code Raster} to dither into.
+ * Only {@code IndexColorModel} allowed.
+ *
+ * @param pSrc
+ *
+ * @return a {@code WritableRaster}
+ */
+ public final WritableRaster createCompatibleDestRaster(Raster pSrc) {
+ return createCompatibleDestRaster(pSrc, getICM(pSrc));
+ }
+
+ public final WritableRaster createCompatibleDestRaster(Raster pSrc,
+ IndexColorModel pIndexColorModel) {
+ return pIndexColorModel.createCompatibleWritableRaster(pSrc.getWidth(), pSrc.getHeight());
+ /*
+ return new BufferedImage(pSrc.getWidth(), pSrc.getHeight(),
+ BufferedImage.TYPE_BYTE_INDEXED,
+ pIndexColorModel).getRaster();
+ */
+ }
+
+
+ /**
+ * Returns the bounding box of the filtered destination image. Since
+ * this is not a geometric operation, the bounding box does not
+ * change.
+ * @param pSrc the {@code BufferedImage} to be filtered
+ * @return the bounds of the filtered definition image.
+ */
+ public final Rectangle2D getBounds2D(BufferedImage pSrc) {
+ return getBounds2D(pSrc.getRaster());
+ }
+
+ /**
+ * Returns the bounding box of the filtered destination Raster. Since
+ * this is not a geometric operation, the bounding box does not
+ * change.
+ * @param pSrc the {@code Raster} to be filtered
+ * @return the bounds of the filtered definition {@code Raster}.
+ */
+ public final Rectangle2D getBounds2D(Raster pSrc) {
+ return pSrc.getBounds();
+ }
+
+ /**
+ * Returns the location of the destination point given a
+ * point in the source. If {@code dstPt} is not
+ * {@code null}, it will be used to hold the return value.
+ * Since this is not a geometric operation, the {@code srcPt}
+ * will equal the {@code dstPt}.
+ * @param pSrcPt a {@code Point2D} that represents a point
+ * in the source image
+ * @param pDstPt a {@code Point2D}that represents the location
+ * in the destination
+ * @return the {@code Point2D} in the destination that
+ * corresponds to the specified point in the source.
+ */
+ public final Point2D getPoint2D(Point2D pSrcPt, Point2D pDstPt) {
+ // Create new Point, if needed
+ if (pDstPt == null) {
+ pDstPt = new Point2D.Float();
+ }
+
+ // Copy location
+ pDstPt.setLocation(pSrcPt.getX(), pSrcPt.getY());
+
+ // Return dest
+ return pDstPt;
+ }
+
+ /**
+ * Returns the rendering mHints for this op.
+ * @return the {@code RenderingHints} object associated
+ * with this op.
+ */
+ public final RenderingHints getRenderingHints() {
+ return null;
+ }
+
+ /**
+ * Converts an int ARGB to int triplet.
+ */
+ private static int[] toRGBArray(int pARGB, int[] pBuffer) {
+ pBuffer[0] = ((pARGB & 0x00ff0000) >> 16);
+ pBuffer[1] = ((pARGB & 0x0000ff00) >> 8);
+ pBuffer[2] = ((pARGB & 0x000000ff));
+ //pBuffer[3] = ((pARGB & 0xff000000) >> 24); // alpha
+
+ return pBuffer;
+ }
+
+ /**
+ * Converts a int triplet to int ARGB.
+ */
+ private static int toIntARGB(int[] pRGB) {
+ return 0xff000000 // All opaque
+ | (pRGB[0] << 16)
+ | (pRGB[1] << 8)
+ | (pRGB[2]);
+ /*
+ | ((int) (pRGB[0] << 16) & 0x00ff0000)
+ | ((int) (pRGB[1] << 8) & 0x0000ff00)
+ | ((int) (pRGB[2] ) & 0x000000ff);
+ */
+ }
+
+
+ /**
+ * Performs a single-input/single-output dither operation, applying basic
+ * Floyd-Steinberg error-diffusion to the image.
+ *
+ * @param pSource the source image
+ * @param pDest the destiantion image
+ *
+ * @return the destination image, or a new image, if {@code pDest} was
+ * {@code null}.
+ */
+ public final BufferedImage filter(BufferedImage pSource,
+ BufferedImage pDest) {
+ // Create destination image, if none provided
+ if (pDest == null) {
+ pDest = createCompatibleDestImage(pSource, getICM(pSource));
+ }
+ else if (!(pDest.getColorModel() instanceof IndexColorModel)) {
+ throw new ImageFilterException("Only IndexColorModel allowed.");
+ }
+
+ // Filter rasters
+ filter(pSource.getRaster(), pDest.getRaster(), (IndexColorModel) pDest.getColorModel());
+
+ return pDest;
+ }
+
+ /**
+ * Performs a single-input/single-output dither operation, applying basic
+ * Floyd-Steinberg error-diffusion to the image.
+ *
+ * @param pSource
+ * @param pDest
+ *
+ * @return the destination raster, or a new raster, if {@code pDest} was
+ * {@code null}.
+ */
+ public final WritableRaster filter(final Raster pSource, WritableRaster pDest) {
+ return filter(pSource, pDest, getICM(pSource));
+ }
+
+ private IndexColorModel getICM(BufferedImage pSource) {
+ return (mIndexColorModel != null ? mIndexColorModel : IndexImage.getIndexColorModel(pSource, 256, IndexImage.TRANSPARENCY_BITMASK));
+ }
+ private IndexColorModel getICM(Raster pSource) {
+ return (mIndexColorModel != null ? mIndexColorModel : createIndexColorModel(pSource));
+ }
+
+ private IndexColorModel createIndexColorModel(Raster pSource) {
+ BufferedImage image = new BufferedImage(pSource.getWidth(), pSource.getHeight(),
+ BufferedImage.TYPE_INT_ARGB);
+ image.setData(pSource);
+ return IndexImage.getIndexColorModel(image, 256, IndexImage.TRANSPARENCY_BITMASK);
+ }
+
+
+
+ /**
+ * Performs a single-input/single-output dither operation, applying basic
+ * Floyd-Steinberg error-diffusion to the image.
+ *
+ * @param pSource
+ * @param pDest
+ * @param pColorModel
+ *
+ * @return the destination raster, or a new raster, if {@code pDest} was
+ * {@code null}.
+ */
+ public final WritableRaster filter(final Raster pSource, WritableRaster pDest,
+ IndexColorModel pColorModel) {
+ int width = pSource.getWidth();
+ int height = pSource.getHeight();
+
+ // Create destination raster if needed
+ if (pDest == null) {
+ pDest = createCompatibleDestRaster(pSource, pColorModel);
+ }
+
+ // Initialize Floyd-Steinberg error vectors.
+ // +2 to handle the previous pixel and next pixel case minimally
+ // When reference for column, add 1 to reference as this buffer is
+ // offset from actual column position by one to allow FS to not check
+ // left/right edge conditions
+ int[][] mCurrErr = new int[width + 2][3];
+ int[][] mNextErr = new int[width + 2][3];
+
+ // Random errors in [-1 .. 1] - for first row
+ for (int i = 0; i < width + 2; i++) {
+ // Note: This is broken for the strange cases where nextInt returns Integer.MIN_VALUE
+ /*
+ mCurrErr[i][0] = (Math.abs(RANDOM.nextInt()) % (FS_SCALE * 2)) - FS_SCALE;
+ mCurrErr[i][1] = (Math.abs(RANDOM.nextInt()) % (FS_SCALE * 2)) - FS_SCALE;
+ mCurrErr[i][2] = (Math.abs(RANDOM.nextInt()) % (FS_SCALE * 2)) - FS_SCALE;
+ */
+ mCurrErr[i][0] = RANDOM.nextInt(FS_SCALE * 2) - FS_SCALE;
+ mCurrErr[i][1] = RANDOM.nextInt(FS_SCALE * 2) - FS_SCALE;
+ mCurrErr[i][2] = RANDOM.nextInt(FS_SCALE * 2) - FS_SCALE;
+ }
+
+ // Temp buffers
+ final int[] diff = new int[3]; // No alpha
+ final int[] inRGB = new int[4];
+ final int[] outRGB = new int[4];
+ Object pixel = null;
+ boolean forward = true;
+
+ // Loop through image data
+ for (int y = 0; y < height; y++) {
+ // Clear out next error rows for colour errors
+ for (int i = mNextErr.length; --i >= 0;) {
+ mNextErr[i][0] = 0;
+ mNextErr[i][1] = 0;
+ mNextErr[i][2] = 0;
+ }
+
+ // Set up start column and limit
+ int x;
+ int limit;
+ if (forward) {
+ x = 0;
+ limit = width;
+ }
+ else {
+ x = width - 1;
+ limit = -1;
+ }
+
+ // TODO: Use getPixels instead of getPixel for better performance?
+
+ // Loop over row
+ while (true) {
+ // Get RGB from original raster
+ // DON'T KNOW IF THIS WILL WORK FOR ALL TYPES.
+ pSource.getPixel(x, y, inRGB);
+
+ // Get error for this pixel & add error to rgb
+ for (int i = 0; i < 3; i++) {
+ // Make a 28.4 FP number, add Error (with fraction),
+ // rounding and truncate to int
+ inRGB[i] = ((inRGB[i] << 4) + mCurrErr[x + 1][i] + 0x08) >> 4;
+
+ // Clamp
+ if (inRGB[i] > 255) {
+ inRGB[i] = 255;
+ }
+ else if (inRGB[i] < 0) {
+ inRGB[i] = 0;
+ }
+ }
+
+ // Get pixel value...
+ // It is VERY important that we are using a IndexColorModel that
+ // support reverse color lookup for speed.
+ pixel = pColorModel.getDataElements(toIntARGB(inRGB), pixel);
+
+ // ...set it...
+ pDest.setDataElements(x, y, pixel);
+
+ // ..and get back the closet match
+ pDest.getPixel(x, y, outRGB);
+
+ // Convert the value to default sRGB
+ // Should work for all transfertypes supported by IndexColorModel
+ toRGBArray(pColorModel.getRGB(outRGB[0]), outRGB);
+
+ // Find diff
+ diff[0] = inRGB[0] - outRGB[0];
+ diff[1] = inRGB[1] - outRGB[1];
+ diff[2] = inRGB[2] - outRGB[2];
+
+ // Apply F-S error diffusion
+ // Serpentine scan: left-right
+ if (forward) {
+ // Row 1 (y)
+ // Update error in this pixel (x + 1)
+ mCurrErr[x + 2][0] += diff[0] * 7;
+ mCurrErr[x + 2][1] += diff[1] * 7;
+ mCurrErr[x + 2][2] += diff[2] * 7;
+
+ // Row 2 (y + 1)
+ // Update error in this pixel (x - 1)
+ mNextErr[x][0] += diff[0] * 3;
+ mNextErr[x][1] += diff[1] * 3;
+ mNextErr[x][2] += diff[2] * 3;
+ // Update error in this pixel (x)
+ mNextErr[x + 1][0] += diff[0] * 5;
+ mNextErr[x + 1][1] += diff[1] * 5;
+ mNextErr[x + 1][2] += diff[2] * 5;
+ // Update error in this pixel (x + 1)
+ // TODO: Consider calculating this using
+ // error term = error - sum(error terms 1, 2 and 3)
+ // See Computer Graphics (Foley et al.), p. 573
+ mNextErr[x + 2][0] += diff[0]; // * 1;
+ mNextErr[x + 2][1] += diff[1]; // * 1;
+ mNextErr[x + 2][2] += diff[2]; // * 1;
+
+ // Next
+ x++;
+
+ // Done?
+ if (x >= limit) {
+ break;
+ }
+
+ }
+ else {
+ // Row 1 (y)
+ // Update error in this pixel (x - 1)
+ mCurrErr[x][0] += diff[0] * 7;
+ mCurrErr[x][1] += diff[1] * 7;
+ mCurrErr[x][2] += diff[2] * 7;
+
+ // Row 2 (y + 1)
+ // Update error in this pixel (x + 1)
+ mNextErr[x + 2][0] += diff[0] * 3;
+ mNextErr[x + 2][1] += diff[1] * 3;
+ mNextErr[x + 2][2] += diff[2] * 3;
+ // Update error in this pixel (x)
+ mNextErr[x + 1][0] += diff[0] * 5;
+ mNextErr[x + 1][1] += diff[1] * 5;
+ mNextErr[x + 1][2] += diff[2] * 5;
+ // Update error in this pixel (x - 1)
+ // TODO: Consider calculating this using
+ // error term = error - sum(error terms 1, 2 and 3)
+ // See Computer Graphics (Foley et al.), p. 573
+ mNextErr[x][0] += diff[0]; // * 1;
+ mNextErr[x][1] += diff[1]; // * 1;
+ mNextErr[x][2] += diff[2]; // * 1;
+
+ // Previous
+ x--;
+
+ // Done?
+ if (x <= limit) {
+ break;
+ }
+ }
+ }
+
+ // Make next error info current for next iteration
+ int[][] temperr;
+ temperr = mCurrErr;
+ mCurrErr = mNextErr;
+ mNextErr = temperr;
+
+ // Toggle direction
+ if (mAlternateScans) {
+ forward = !forward;
+ }
+ }
+ return pDest;
+ }
+}
\ No newline at end of file
diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/EasyImage.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/EasyImage.java
new file mode 100755
index 00000000..71427244
--- /dev/null
+++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/EasyImage.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (c) 2008, Harald Kuhr
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * Neither the name "TwelveMonkeys" nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.twelvemonkeys.image;
+
+import javax.imageio.ImageIO;
+import java.awt.*;
+import java.awt.image.*;
+import java.awt.image.renderable.RenderableImage;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * EasyImage
+ *
+ *
+ * @author Harald Kuhr
+ * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/EasyImage.java#1 $
+ */
+public class EasyImage extends BufferedImage {
+ public EasyImage(InputStream pInput) throws IOException {
+ this(ImageIO.read(pInput));
+ }
+
+ public EasyImage(BufferedImage pImage) {
+ this(pImage.getColorModel(), pImage.getRaster());
+ }
+
+ public EasyImage(RenderableImage pImage) {
+ this(pImage.createDefaultRendering());
+ }
+
+ public EasyImage(RenderedImage pImage) {
+ this(pImage.getColorModel(), pImage.copyData(pImage.getColorModel().createCompatibleWritableRaster(pImage.getWidth(), pImage.getHeight())));
+ }
+
+ public EasyImage(ImageProducer pImage) {
+ this(new BufferedImageFactory(pImage).getBufferedImage());
+ }
+
+ public EasyImage(Image pImage) {
+ this(new BufferedImageFactory(pImage).getBufferedImage());
+ }
+
+ private EasyImage(ColorModel cm, WritableRaster raster) {
+ super(cm, raster, cm.isAlphaPremultiplied(), null);
+ }
+
+ public boolean write(String pFormat, OutputStream pOutput) throws IOException {
+ return ImageIO.write(this, pFormat, pOutput);
+ }
+}
diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ExtendedImageConsumer.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ExtendedImageConsumer.java
new file mode 100755
index 00000000..949d6932
--- /dev/null
+++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ExtendedImageConsumer.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2008, Harald Kuhr
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * Neither the name "TwelveMonkeys" nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.twelvemonkeys.image;
+
+import java.awt.image.ImageConsumer;
+import java.awt.image.ColorModel;
+
+/**
+ * ExtendedImageConsumer
+ *
+ *
+ * @author Harald Kuhr
+ * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ExtendedImageConsumer.java#1 $
+ */
+public interface ExtendedImageConsumer extends ImageConsumer {
+ /**
+ *
+ * @param pX
+ * @param pY
+ * @param pWidth
+ * @param pHeight
+ * @param pModel
+ * @param pPixels
+ * @param pOffset
+ * @param pScanSize
+ */
+ public void setPixels(int pX, int pY, int pWidth, int pHeight,
+ ColorModel pModel,
+ short[] pPixels, int pOffset, int pScanSize);
+
+ // Allow for packed and interleaved models
+ public void setPixels(int pX, int pY, int pWidth, int pHeight,
+ ColorModel pModel,
+ byte[] pPixels, int pOffset, int pScanSize);
+}
diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/GraphicsUtil.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/GraphicsUtil.java
new file mode 100755
index 00000000..f221bdb5
--- /dev/null
+++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/GraphicsUtil.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (c) 2008, Harald Kuhr
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * Neither the name "TwelveMonkeys" nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.twelvemonkeys.image;
+
+import java.awt.*;
+
+/**
+ * GraphicsUtil
+ *
+ * @author Harald Kuhr
+ * @author last modified by $Author: haku $
+ * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/GraphicsUtil.java#1 $
+ */
+public final class GraphicsUtil {
+
+ /**
+ * Enables anti-aliasing in the {@code Graphics} object.
+ *
+ * Anti-aliasing is enabled by casting to {@code Graphics2D} and setting
+ * the rendering hint {@code RenderingHints.KEY_ANTIALIASING} to
+ * {@code RenderingHints.VALUE_ANTIALIAS_ON}.
+ *
+ * @param pGraphics the graphics object
+ * @throws ClassCastException if {@code pGraphics} is not an instance of
+ * {@code Graphics2D}.
+ *
+ * @see java.awt.RenderingHints#KEY_ANTIALIASING
+ */
+ public static void enableAA(final Graphics pGraphics) {
+ ((Graphics2D) pGraphics).setRenderingHint(
+ RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON
+ );
+ }
+
+ /**
+ * Sets the alpha in the {@code Graphics} object.
+ *
+ * Alpha is set by casting to {@code Graphics2D} and setting the composite
+ * to the rule {@code AlphaComposite.SRC_OVER} multiplied by the given
+ * alpha.
+ *
+ * @param pGraphics the graphics object
+ * @param pAlpha the alpha level, {@code alpha} must be a floating point
+ * number in the inclusive range [0.0, 1.0].
+ * @throws ClassCastException if {@code pGraphics} is not an instance of
+ * {@code Graphics2D}.
+ *
+ * @see java.awt.AlphaComposite#SRC_OVER
+ * @see java.awt.AlphaComposite#getInstance(int, float)
+ */
+ public static void setAlpha(final Graphics pGraphics, final float pAlpha) {
+ ((Graphics2D) pGraphics).setComposite(
+ AlphaComposite.getInstance(AlphaComposite.SRC_OVER, pAlpha)
+ );
+ }
+}
diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/GrayColorModel.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/GrayColorModel.java
new file mode 100755
index 00000000..c636e9fc
--- /dev/null
+++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/GrayColorModel.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2008, Harald Kuhr
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * Neither the name "TwelveMonkeys" nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.twelvemonkeys.image;
+
+import java.awt.image.*;
+
+/**
+ * This class represents a 256 color fixed grayscale IndexColorModel.
+ *
+ * @author Harald Kuhr
+ * @author last modified by $Author: haku $
+ *
+ * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/GrayColorModel.java#1 $
+ *
+ */
+public class GrayColorModel extends IndexColorModel {
+
+ private final static byte[] sGrays = createGrayScale();
+
+ public GrayColorModel() {
+ super(8, sGrays.length, sGrays, sGrays, sGrays);
+ }
+
+ private static byte[] createGrayScale() {
+ byte[] grays = new byte[256];
+ for (int i = 0; i < 256; i++) {
+ grays[i] = (byte) i;
+ }
+ return grays;
+ }
+
+}
diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/GrayFilter.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/GrayFilter.java
new file mode 100755
index 00000000..dd4a8ef2
--- /dev/null
+++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/GrayFilter.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (c) 2008, Harald Kuhr
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * Neither the name "TwelveMonkeys" nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.twelvemonkeys.image;
+
+import java.awt.image.*;
+
+/**
+ * This class can convert a color image to grayscale.
+ *
+ * Uses ITU standard conversion: (222 * Red + 707 * Green + 71 * Blue) / 1000.
+ *
+ * @author Harald Kuhr
+ * @author last modified by $Author: haku $
+ *
+ * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/GrayFilter.java#1 $
+ *
+ */
+public class GrayFilter extends RGBImageFilter {
+
+ // This filter can filter IndexColorModel
+ {
+ canFilterIndexColorModel = true;
+ }
+
+ private int mLow = 0;
+ private float mRange = 1.0f;
+
+ /**
+ * Constructs a GrayFilter using ITU color-conversion.
+ */
+ public GrayFilter() {
+ }
+
+ /**
+ * Constructs a GrayFilter using ITU color-conversion, and a dynamic range between
+ * pLow and pHigh.
+ *
+ * @param pLow float in the range 0..1
+ * @param pHigh float in the range 0..1 and >= pLow
+ */
+ public GrayFilter(float pLow, float pHigh) {
+ if (pLow > pHigh) {
+ pLow = 0f;
+ }
+ // Make sure high and low are inside range
+ if (pLow < 0f) {
+ pLow = 0f;
+ }
+ else if (pLow > 1f) {
+ pLow = 1f;
+ }
+ if (pHigh < 0f) {
+ pHigh = 0f;
+ }
+ else if (pHigh > 1f) {
+ pHigh = 1f;
+ }
+
+ mLow = (int) (pLow * 255f);
+ mRange = pHigh - pLow;
+
+ }
+
+ /**
+ * Constructs a GrayFilter using ITU color-conversion, and a dynamic
+ * range between pLow and pHigh.
+ *
+ * @param pLow integer in the range 0..255
+ * @param pHigh inteeger in the range 0..255 and >= pLow
+ */
+ public GrayFilter(int pLow, int pHigh) {
+ this(pLow / 255f, pHigh / 255f);
+ }
+
+ /**
+ * Filters one pixel using ITU color-conversion.
+ *
+ * @param pX x
+ * @param pY y
+ * @param pARGB pixel value in default color space
+ *
+ * @return the filtered pixel value in the default color space
+ */
+ public int filterRGB(int pX, int pY, int pARGB) {
+ // Get color components
+ int r = pARGB >> 16 & 0xFF;
+ int g = pARGB >> 8 & 0xFF;
+ int b = pARGB & 0xFF;
+
+ // ITU standard: Gray scale=(222*Red+707*Green+71*Blue)/1000
+ int gray = (222 * r + 707 * g + 71 * b) / 1000;
+
+ //int gray = (int) ((float) (r + g + b) / 3.0f);
+
+ if (mRange != 1.0f) {
+ // Apply range
+ gray = mLow + (int) (gray * mRange);
+ }
+
+ // Return ARGB pixel
+ return (pARGB & 0xFF000000) | (gray << 16) | (gray << 8) | gray;
+ }
+}
diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ImageConversionException.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ImageConversionException.java
new file mode 100755
index 00000000..aefa2f2c
--- /dev/null
+++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ImageConversionException.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2008, Harald Kuhr
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * Neither the name "TwelveMonkeys" nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.twelvemonkeys.image;
+
+/**
+ * This class wraps IllegalArgumentException, and is thrown by the ImageUtil
+ * class, when trying to convert images read from {@code null}-sources etc.
+ *
+ * @author Harald Kuhr
+ *
+ * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ImageConversionException.java#1 $
+ */
+public class ImageConversionException extends ImageFilterException {
+
+ public ImageConversionException(String pMessage) {
+ super(pMessage);
+ }
+
+ public ImageConversionException(String pMessage, Throwable pCause) {
+ super(pMessage, pCause);
+ }
+}
diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ImageFilterException.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ImageFilterException.java
new file mode 100755
index 00000000..d91bf053
--- /dev/null
+++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ImageFilterException.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2008, Harald Kuhr
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * Neither the name "TwelveMonkeys" nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.twelvemonkeys.image;
+
+/**
+ * This class wraps IllegalArgumentException as thrown by the
+ * BufferedImageOp interface for more fine-grained control.
+ *
+ * @author Harald Kuhr
+ *
+ * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ImageFilterException.java#1 $
+ */
+public class ImageFilterException extends IllegalArgumentException {
+ private Throwable mCause = null;
+
+ public ImageFilterException(String pStr) {
+ super(pStr);
+ }
+
+ public ImageFilterException(Throwable pT) {
+ initCause(pT);
+ }
+
+ public ImageFilterException(String pStr, Throwable pT) {
+ super(pStr);
+ initCause(pT);
+ }
+
+ public Throwable initCause(Throwable pThrowable) {
+ if (mCause != null) {
+ // May only be called once
+ throw new IllegalStateException();
+ }
+ else if (pThrowable == this) {
+ throw new IllegalArgumentException();
+ }
+
+ mCause = pThrowable;
+
+ // Hmmm...
+ return this;
+ }
+
+ public Throwable getCause() {
+ return mCause;
+ }
+}
diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ImageUtil.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ImageUtil.java
new file mode 100755
index 00000000..8c2ae031
--- /dev/null
+++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ImageUtil.java
@@ -0,0 +1,2037 @@
+/*
+ * Copyright (c) 2008, Harald Kuhr
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * Neither the name "TwelveMonkeys" nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.twelvemonkeys.image;
+
+import java.awt.*;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Rectangle2D;
+import java.awt.image.*;
+
+import java.util.Hashtable;
+
+/**
+ * This class contains methods for basic image manipulation and conversion.
+ *
+ * @todo Split palette generation out, into ColorModel classes.
+ *
+ * @author Harald Kuhr
+ * @author last modified by $Author: haku $
+ * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ImageUtil.java#3 $
+ */
+public final class ImageUtil {
+
+ public final static int ROTATE_90_CCW = -90;
+ public final static int ROTATE_90_CW = 90;
+ public final static int ROTATE_180 = 180;
+
+ public final static int FLIP_VERTICAL = -1;
+ public final static int FLIP_HORIZONTAL = 1;
+
+ /**
+ * Alias for {@link ConvolveOp#EDGE_ZERO_FILL}.
+ * @see #convolve(java.awt.image.BufferedImage, java.awt.image.Kernel, int)
+ * @see #EDGE_REFLECT
+ */
+ public static final int EDGE_ZERO_FILL = ConvolveOp.EDGE_ZERO_FILL;
+ /**
+ * Alias for {@link ConvolveOp#EDGE_NO_OP}.
+ * @see #convolve(java.awt.image.BufferedImage, java.awt.image.Kernel, int)
+ * @see #EDGE_REFLECT
+ */
+ public static final int EDGE_NO_OP = ConvolveOp.EDGE_NO_OP;
+ /**
+ * Adds a border to the image while convolving. The border will reflect the
+ * edges of the original image. This is usually a good default.
+ * Note that while this mode typically provides better quality than the
+ * standard modes {@code EDGE_ZERO_FILL} and {@code EDGE_NO_OP}, it does so
+ * at the expense of higher memory consumption and considerable more computation.
+ * @see #convolve(java.awt.image.BufferedImage, java.awt.image.Kernel, int)
+ */
+ public static final int EDGE_REFLECT = 2; // as JAI BORDER_REFLECT
+ /**
+ * Adds a border to the image while convolving. The border will wrap the
+ * edges of the original image. This is usually the best choice for tiles.
+ * Note that while this mode typically provides better quality than the
+ * standard modes {@code EDGE_ZERO_FILL} and {@code EDGE_NO_OP}, it does so
+ * at the expense of higher memory consumption and considerable more computation.
+ * @see #convolve(java.awt.image.BufferedImage, java.awt.image.Kernel, int)
+ * @see #EDGE_REFLECT
+ */
+ public static final int EDGE_WRAP = 3; // as JAI BORDER_WRAP
+
+ /**
+ * Java default dither
+ */
+ public final static int DITHER_DEFAULT = IndexImage.DITHER_DEFAULT;
+
+ /**
+ * No dither
+ */
+ public final static int DITHER_NONE = IndexImage.DITHER_NONE;
+
+ /**
+ * Error diffusion dither
+ */
+ public final static int DITHER_DIFFUSION = IndexImage.DITHER_DIFFUSION;
+
+ /**
+ * Error diffusion dither with alternating scans
+ */
+ public final static int DITHER_DIFFUSION_ALTSCANS = IndexImage.DITHER_DIFFUSION_ALTSCANS;
+
+ /**
+ * Default color selection
+ */
+ public final static int COLOR_SELECTION_DEFAULT = IndexImage.COLOR_SELECTION_DEFAULT;
+
+ /**
+ * Prioritize speed
+ */
+ public final static int COLOR_SELECTION_FAST = IndexImage.COLOR_SELECTION_FAST;
+
+ /**
+ * Prioritize quality
+ */
+ public final static int COLOR_SELECTION_QUALITY = IndexImage.COLOR_SELECTION_QUALITY;
+
+ /**
+ * Default transparency (none)
+ */
+ public final static int TRANSPARENCY_DEFAULT = IndexImage.TRANSPARENCY_DEFAULT;
+
+ /**
+ * Discard any alpha information
+ */
+ public final static int TRANSPARENCY_OPAQUE = IndexImage.TRANSPARENCY_OPAQUE;
+
+ /**
+ * Convert alpha to bitmask
+ */
+ public final static int TRANSPARENCY_BITMASK = IndexImage.TRANSPARENCY_BITMASK;
+
+ /**
+ * Keep original alpha (not supported yet)
+ */
+ protected final static int TRANSPARENCY_TRANSLUCENT = IndexImage.TRANSPARENCY_TRANSLUCENT;
+
+ /** Passed to the createXxx methods, to indicate that the type does not matter */
+ private final static int BI_TYPE_ANY = -1;
+ /*
+ public final static int BI_TYPE_ANY_TRANSLUCENT = -1;
+ public final static int BI_TYPE_ANY_BITMASK = -2;
+ public final static int BI_TYPE_ANY_OPAQUE = -3;*/
+
+ /** Tells wether this WM may support acceleration of some images */
+ private static boolean VM_SUPPORTS_ACCELERATION = true;
+
+ /** The sharpen matrix */
+ private static final float[] SHARPEN_MATRIX = new float[] {
+ 0.0f, -0.3f, 0.0f,
+ -0.3f, 2.2f, -0.3f,
+ 0.0f, -0.3f, 0.0f
+ };
+
+ /**
+ * The sharpen kernel. Uses the following 3 by 3 matrix:
+ *
+ *
0.0
-0.3
0.0
+ *
-0.3
2.2
-0.3
+ *
0.0
-0.3
0.0
+ *
+ */
+ private static final Kernel SHARPEN_KERNEL = new Kernel(3, 3, SHARPEN_MATRIX);
+
+ /**
+ * Component that can be used with the MediaTracker etc.
+ */
+ private static final Component NULL_COMPONENT = new Component() {};
+
+ /** Our static image tracker */
+ private static MediaTracker sTracker = new MediaTracker(NULL_COMPONENT);
+ //private static Object sTrackerMutex = new Object();
+
+ /** Image id used by the image tracker */
+ //private static int sTrackerId = 0;
+
+ /** */
+ protected static final AffineTransform IDENTITY_TRANSFORM = new AffineTransform();
+ /** */
+ protected static final Point LOCATION_UPPER_LEFT = new Point(0, 0);
+
+ /** */
+ private static final boolean COLORMODEL_TRANSFERTYPE_SUPPORTED = isColorModelTransferTypeSupported();
+
+ /** */
+ private static final GraphicsConfiguration DEFAULT_CONFIGURATION = getDefaultGraphicsConfiguration();
+
+ private static GraphicsConfiguration getDefaultGraphicsConfiguration() {
+ try {
+ GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment();
+ if (!env.isHeadlessInstance()) {
+ return env.getDefaultScreenDevice().getDefaultConfiguration();
+ }
+ }
+ catch (LinkageError e) {
+ // Means we are not in a 1.4+ VM, so skip testing for headless again
+ VM_SUPPORTS_ACCELERATION = false;
+ }
+ return null;
+ }
+
+ /** Creates an ImageUtil. Private constructor. */
+ private ImageUtil() {
+ }
+
+ /**
+ * Tests if {@code ColorModel} has a {@code getTransferType} method.
+ *
+ * @return {@code true} if {@code ColorModel} has a
+ * {@code getTransferType} method
+ */
+ private static boolean isColorModelTransferTypeSupported() {
+ try {
+ ColorModel.getRGBdefault().getTransferType();
+ return true;
+ }
+ catch (Throwable t) {
+ return false;
+ }
+ }
+
+ /**
+ * Converts the {@code RenderedImage} to a {@code BufferedImage}.
+ * The new image will have the same {@code ColorModel},
+ * {@code Raster} and properties as the original image, if possible.
+ *
+ * If the image is allready a {@code BufferedImage}, it is simply returned
+ * and no conversion takes place.
+ *
+ * @param pOriginal the image to convert.
+ *
+ * @return a {@code BufferedImage}
+ */
+ public static BufferedImage toBuffered(RenderedImage pOriginal) {
+ // Don't convert if it allready is a BufferedImage
+ if (pOriginal instanceof BufferedImage) {
+ return (BufferedImage) pOriginal;
+ }
+ if (pOriginal == null) {
+ throw new IllegalArgumentException("original == null");
+ }
+
+ // Copy properties
+ Hashtable properties;
+ String[] names = pOriginal.getPropertyNames();
+ if (names != null && names.length > 0) {
+ properties = new Hashtable(names.length);
+
+ for (String name : names) {
+ properties.put(name, pOriginal.getProperty(name));
+ }
+ }
+ else {
+ properties = null;
+ }
+
+ // NOTE: This is a workaround for the broken Batik '*Red' classes, that
+ // throw NPE if copyData(null) is used. This may actually be faster too.
+ // See RenderedImage#copyData / RenderedImage#getData
+ Raster data = pOriginal.getData();
+ WritableRaster raster;
+ if (data instanceof WritableRaster) {
+ raster = (WritableRaster) data;
+ }
+ else {
+ raster = data.createCompatibleWritableRaster();
+ raster = pOriginal.copyData(raster);
+ }
+
+ // Create buffered image
+ ColorModel colorModel = pOriginal.getColorModel();
+ return new BufferedImage(colorModel, raster,
+ colorModel.isAlphaPremultiplied(),
+ properties);
+ }
+
+ /**
+ * Converts the {@code RenderedImage} to a {@code BufferedImage} of the
+ * given type.
+ *
+ * If the image is allready a {@code BufferedImage} of the given type, it
+ * is simply returned and no conversion takes place.
+ *
+ * @param pOriginal the image to convert.
+ * @param pType the type of buffered image
+ *
+ * @return a {@code BufferedImage}
+ *
+ * @throws IllegalArgumentException if {@code pOriginal == null}
+ * or {@code pType} is not a valid type for {@code BufferedImage}
+ *
+ * @see java.awt.image.BufferedImage#getType()
+ */
+ public static BufferedImage toBuffered(RenderedImage pOriginal, int pType) {
+ // Don't convert if it allready is BufferedImage and correct type
+ if ((pOriginal instanceof BufferedImage) && ((BufferedImage) pOriginal).getType() == pType) {
+ return (BufferedImage) pOriginal;
+ }
+ if (pOriginal == null) {
+ throw new IllegalArgumentException("original == null");
+ }
+
+ // Create a buffered image
+ BufferedImage image = createBuffered(pOriginal.getWidth(),
+ pOriginal.getHeight(),
+ pType, Transparency.TRANSLUCENT);
+
+ // Draw the image onto the buffer
+ // NOTE: This is faster than doing a raster conversion in most cases
+ Graphics2D g = image.createGraphics();
+ try {
+ g.setComposite(AlphaComposite.Src);
+ g.drawRenderedImage(pOriginal, IDENTITY_TRANSFORM);
+ }
+ finally {
+ g.dispose();
+ }
+
+ return image;
+ }
+
+ /**
+ * Converts the {@code BufferedImage} to a {@code BufferedImage} of the
+ * given type. The new image will have the same {@code ColorModel},
+ * {@code Raster} and properties as the original image, if possible.
+ *
+ * If the image is allready a {@code BufferedImage} of the given type, it
+ * is simply returned and no conversion takes place.
+ *
+ * This method simply invokes
+ * {@link #toBuffered(RenderedImage,int) toBuffered((RenderedImage) pOriginal, pType)}.
+ *
+ * @param pOriginal the image to convert.
+ * @param pType the type of buffered image
+ *
+ * @return a {@code BufferedImage}
+ *
+ * @throws IllegalArgumentException if {@code pOriginal == null}
+ * or if {@code pType} is not a valid type for {@code BufferedImage}
+ *
+ * @see java.awt.image.BufferedImage#getType()
+ */
+ public static BufferedImage toBuffered(BufferedImage pOriginal, int pType) {
+ return toBuffered((RenderedImage) pOriginal, pType);
+ }
+
+ /**
+ * Converts the {@code Image} to a {@code BufferedImage}.
+ * The new image will have the same {@code ColorModel}, {@code Raster} and
+ * properties as the original image, if possible.
+ *
+ * If the image is allready a {@code BufferedImage}, it is simply returned
+ * and no conversion takes place.
+ *
+ * @param pOriginal the image to convert.
+ *
+ * @return a {@code BufferedImage}
+ *
+ * @throws IllegalArgumentException if {@code pOriginal == null}
+ * @throws ImageConversionException if the image cannot be converted
+ */
+ public static BufferedImage toBuffered(Image pOriginal) {
+ // Don't convert if it allready is BufferedImage
+ if (pOriginal instanceof BufferedImage) {
+ return (BufferedImage) pOriginal;
+ }
+ if (pOriginal == null) {
+ throw new IllegalArgumentException("original == null");
+ }
+
+ //System.out.println("--> Doing full BufferedImage conversion...");
+
+ BufferedImageFactory factory = new BufferedImageFactory(pOriginal);
+ return factory.getBufferedImage();
+ }
+
+ /**
+ * Creates a copy of the given image. The image will have the same
+ * colormodel and raster type, but will not share image (pixel) data.
+ *
+ * @param pImage the image to clone.
+ *
+ * @return a new {@code BufferedImage}
+ *
+ * @throws IllegalArgumentException if {@code pImage} is {@code null}
+ */
+ public static BufferedImage createCopy(final BufferedImage pImage) {
+ if (pImage == null) {
+ throw new IllegalArgumentException("image == null");
+ }
+
+ ColorModel cm = pImage.getColorModel();
+
+ BufferedImage img = new BufferedImage(cm,
+ cm.createCompatibleWritableRaster(pImage.getWidth(), pImage.getHeight()),
+ cm.isAlphaPremultiplied(), null);
+
+ drawOnto(pImage, img);
+
+ return img;
+ }
+
+ /**
+ * Creates a {@code WritableRaster} for the given {@code ColorModel} and
+ * pixel data.
+ *
+ * This method is optimized for the most common cases of {@code ColorModel}
+ * and pixel data combinations. The raster's backing {@code DataBuffer} is
+ * created directly from the pixel data, as this is faster and with more
+ * resource-friendly than using
+ * {@code ColorModel.createCompatibleWritableRaster(w, h)}.
+ *
+ * For unknown combinations, the method will fallback to using
+ * {@code ColorModel.createCompatibleWritableRaster(w, h)} and
+ * {@code WritableRaster.setDataElements(w, h, pixels)}
+ *
+ * Note that the {@code ColorModel} and pixel data are not cloned
+ * (in most cases).
+ *
+ * @param pWidth the requested raster width
+ * @param pHeight the requested raster height
+ * @param pPixels the pixels, as an array, of a type supported by the
+ * different {@link DataBuffer}
+ * @param pColorModel the color model to use
+ * @return a new {@code WritableRaster}
+ *
+ * @throws NullPointerException if either {@code pColorModel} or
+ * {@code pPixels} are {@code null}.
+ * @throws RuntimeException if {@code pWidth} and {@code pHeight} does not
+ * match the pixel data in {@code pPixels}.
+ *
+ * @see ColorModel#createCompatibleWritableRaster(int, int)
+ * @see ColorModel#createCompatibleSampleModel(int, int)
+ * @see WritableRaster#setDataElements(int, int, Object)
+ * @see DataBuffer
+ */
+ static WritableRaster createRaster(int pWidth, int pHeight, Object pPixels, ColorModel pColorModel) {
+ // NOTE: This is optimized code for most common cases.
+ // We create a DataBuffer with the array from grabber.getPixels()
+ // directly, and creating a raster based on the ColorModel.
+ // Creating rasters this way is faster and more resource-friendly, as
+ // cm.createCompatibleWritableRaster allocates an
+ // "empty" DataBuffer with a storage array of w*h. This array is
+ // later discarded, and replaced in the raster.setDataElements() call.
+ // The "old" way is kept as a more compatible fall-back mode.
+
+ DataBuffer buffer = null;
+ WritableRaster raster = null;
+
+ int bands;
+ if (pPixels instanceof int[]) {
+ int[] data = (int[]) pPixels;
+ buffer = new DataBufferInt(data, data.length);
+ //bands = data.length / (w * h);
+ bands = pColorModel.getNumComponents();
+ }
+ else if (pPixels instanceof short[]) {
+ short[] data = (short[]) pPixels;
+ buffer = new DataBufferUShort(data, data.length);
+ bands = data.length / (pWidth * pHeight);
+ //bands = cm.getNumComponents();
+ }
+ else if (pPixels instanceof byte[]) {
+ byte[] data = (byte[]) pPixels;
+ buffer = new DataBufferByte(data, data.length);
+
+ // NOTE: This only holds for gray and indexed with one byte per pixel...
+ if (pColorModel instanceof IndexColorModel) {
+ bands = 1;
+ }
+ else {
+ bands = data.length / (pWidth * pHeight);
+ }
+
+ //bands = pColorModel.getNumComponents();
+ //System.out.println("Pixels: " + data.length + " (" + buffer.getSize() + ")");
+ //System.out.println("w*h*bands: " + (pWidth * pHeight * bands));
+ //System.out.println("Bands: " + bands);
+ //System.out.println("Numcomponents: " + pColorModel.getNumComponents());
+ }
+ else {
+ //System.out.println("Fallback!");
+ // Fallback mode, slower & requires more memory, but compatible
+ bands = -1;
+
+ // Create raster from colormodel, w and h
+ raster = pColorModel.createCompatibleWritableRaster(pWidth, pHeight);
+ raster.setDataElements(0, 0, pWidth, pHeight, pPixels); // Note: This is known to throw ClassCastExceptions..
+ }
+
+ //System.out.println("Bands: " + bands);
+ //System.out.println("Pixels: " + pixels.getClass() + " length: " + buffer.getSize());
+ //System.out.println("Needed Raster: " + cm.createCompatibleWritableRaster(1, 1));
+
+ if (raster == null) {
+ //int bits = cm.getPixelSize();
+ //if (bits > 4) {
+ if (pColorModel instanceof IndexColorModel && isIndexedPacked((IndexColorModel) pColorModel)) {
+ //System.out.println("Creating packed indexed model");
+ raster = Raster.createPackedRaster(buffer, pWidth, pHeight, pColorModel.getPixelSize(), LOCATION_UPPER_LEFT);
+ }
+ else if (pColorModel instanceof PackedColorModel) {
+ //System.out.println("Creating packed model");
+ PackedColorModel pcm = (PackedColorModel) pColorModel;
+ raster = Raster.createPackedRaster(buffer, pWidth, pHeight, pWidth, pcm.getMasks(), LOCATION_UPPER_LEFT);
+ }
+ else {
+ //System.out.println("Creating interleaved model");
+ // (A)BGR order... For TYPE_3BYTE_BGR/TYPE_4BYTE_ABGR/TYPE_4BYTE_ABGR_PRE.
+ int[] bandsOffsets = new int[bands];
+ for (int i = 0; i < bands;) {
+ bandsOffsets[i] = bands - (++i);
+ }
+ //System.out.println("zzz Data array: " + buffer.getSize());
+
+ raster = Raster.createInterleavedRaster(buffer, pWidth, pHeight, pWidth * bands, bands, bandsOffsets, LOCATION_UPPER_LEFT);
+ }
+ }
+
+ return raster;
+ }
+
+ private static boolean isIndexedPacked(IndexColorModel pColorModel) {
+ return (pColorModel.getPixelSize() == 1 || pColorModel.getPixelSize() == 2 || pColorModel.getPixelSize() == 4);
+ }
+
+ /**
+ * Workaround for bug: TYPE_3BYTE_BGR, TYPE_4BYTE_ABGR and
+ * TYPE_4BYTE_ABGR_PRE are all converted to TYPE_CUSTOM when using the
+ * default createCompatibleWritableRaster from ComponentColorModel.
+ *
+ * @param pOriginal the orignal image
+ * @param pModel the original color model
+ * @param mWidth the requested width of the raster
+ * @param mHeight the requested height of the raster
+ *
+ * @return a new WritableRaster
+ */
+ static WritableRaster createCompatibleWritableRaster(BufferedImage pOriginal, ColorModel pModel, int mWidth, int mHeight) {
+ if (pModel == null || equals(pOriginal.getColorModel(), pModel)) {
+ switch (pOriginal.getType()) {
+ case BufferedImage.TYPE_3BYTE_BGR:
+ int[] bOffs = {2, 1, 0}; // NOTE: These are reversed from what the cm.createCompatibleWritableRaster would return
+ return Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE,
+ mWidth, mHeight,
+ mWidth * 3, 3,
+ bOffs, null);
+ case BufferedImage.TYPE_4BYTE_ABGR:
+ case BufferedImage.TYPE_4BYTE_ABGR_PRE:
+ bOffs = new int[] {3, 2, 1, 0}; // NOTE: These are reversed from what the cm.createCompatibleWritableRaster would return
+ return Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE,
+ mWidth, mHeight,
+ mWidth * 4, 4,
+ bOffs, null);
+ default:
+ return pOriginal.getColorModel().createCompatibleWritableRaster(mWidth, mHeight);
+ }
+ }
+ return pModel.createCompatibleWritableRaster(mWidth, mHeight);
+ }
+
+ /**
+ * Converts the {@code Image} to a {@code BufferedImage} of the given type.
+ * The new image will have the same {@code ColorModel}, {@code Raster} and
+ * properties as the original image, if possible.
+ *
+ * If the image is allready a {@code BufferedImage} of the given type, it
+ * is simply returned and no conversion takes place.
+ *
+ * @param pOriginal the image to convert.
+ * @param pType the type of buffered image
+ *
+ * @return a {@code BufferedImage}
+ *
+ * @throws IllegalArgumentException if {@code pOriginal == null}
+ * or if {@code pType} is not a valid type for {@code BufferedImage}
+ *
+ * @see java.awt.image.BufferedImage#getType()
+ */
+ public static BufferedImage toBuffered(Image pOriginal, int pType) {
+ return toBuffered(pOriginal, pType, null);
+ }
+
+ /**
+ *
+ * @param pOriginal the original image
+ * @param pType the type of {@code BufferedImage} to create
+ * @param pICM the optional {@code IndexColorModel} to use. If not
+ * {@code null} the {@code pType} must be compatible with the color model
+ * @return a {@code BufferedImage}
+ * @throws IllegalArgumentException if {@code pType} is not compatible with
+ * the color model
+ */
+ private static BufferedImage toBuffered(Image pOriginal, int pType, IndexColorModel pICM) {
+ // Don't convert if it allready is BufferedImage and correct type
+ if ((pOriginal instanceof BufferedImage)
+ && ((BufferedImage) pOriginal).getType() == pType
+ && (pICM == null || equals(((BufferedImage) pOriginal).getColorModel(), pICM))) {
+ return (BufferedImage) pOriginal;
+ }
+ if (pOriginal == null) {
+ throw new IllegalArgumentException("original == null");
+ }
+
+ //System.out.println("--> Doing full BufferedImage conversion, using Graphics.drawImage().");
+
+ // Create a buffered image
+ // NOTE: The getWidth and getHeight methods, will wait for the image
+ BufferedImage image;
+ if (pICM == null) {
+ image = createBuffered(getWidth(pOriginal), getHeight(pOriginal), pType, Transparency.TRANSLUCENT);//new BufferedImage(getWidth(pOriginal), getHeight(pOriginal), pType);
+ }
+ else {
+ image = new BufferedImage(getWidth(pOriginal), getHeight(pOriginal), pType, pICM);
+ }
+
+ // Draw the image onto the buffer
+ drawOnto(image, pOriginal);
+
+ return image;
+ }
+
+ /**
+ * Draws the source image onto the buffered image, using
+ * {@code AlphaComposite.Src} and coordinates {@code 0, 0}.
+ *
+ * @param pImage the image to draw on
+ * @param pSource the source image to draw
+ *
+ * @throws NullPointerException if {@code pImage} or {@code pSource} is
+ * {@code null}
+ */
+ static void drawOnto(final BufferedImage pImage, final Image pSource) {
+ Graphics2D g = pImage.createGraphics();
+ try {
+ g.setComposite(AlphaComposite.Src);
+ g.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_DISABLE);
+ g.drawImage(pSource, 0, 0, null);
+ }
+ finally {
+ g.dispose();
+ }
+ }
+
+ /**
+ * Creates a flipped version of the given image.
+ *
+ * @param pImage the image to flip
+ * @param pAxis the axis to flip around
+ * @return a new {@code BufferedImage}
+ */
+ public static BufferedImage createFlipped(final Image pImage, final int pAxis) {
+ switch (pAxis) {
+ case FLIP_HORIZONTAL:
+ case FLIP_VERTICAL:
+ // TODO case FLIP_BOTH:?? same as rotate 180?
+ break;
+ default:
+ throw new IllegalArgumentException("Illegal direction: " + pAxis);
+ }
+ BufferedImage source = toBuffered(pImage);
+ AffineTransform transform;
+ if (pAxis == FLIP_HORIZONTAL) {
+ transform = AffineTransform.getTranslateInstance(0, source.getHeight());
+ transform.scale(1, -1);
+ }
+ else {
+ transform = AffineTransform.getTranslateInstance(source.getWidth(), 0);
+ transform.scale(-1, 1);
+ }
+ AffineTransformOp transformOp = new AffineTransformOp(transform, AffineTransformOp.TYPE_NEAREST_NEIGHBOR);
+ return transformOp.filter(source, null);
+ }
+
+
+ /**
+ * Rotates the image 90 degrees, clockwise (aka "rotate right"),
+ * counter-clockwise (aka "rotate left") or 180 degrees, depending on the
+ * {@code pDirection} argument.
+ *
+ * The new image will be completely covered with pixels from the source
+ * image.
+ *
+ * @param pImage the source image.
+ * @param pDirection the direction, must be either {@link #ROTATE_90_CW},
+ * {@link #ROTATE_90_CCW} or {@link #ROTATE_180}
+ *
+ * @return a new {@code BufferedImage}
+ *
+ */
+ public static BufferedImage createRotated(final Image pImage, final int pDirection) {
+ switch (pDirection) {
+ case ROTATE_90_CW:
+ case ROTATE_90_CCW:
+ case ROTATE_180:
+ return createRotated(pImage, Math.toRadians(pDirection));
+ default:
+ throw new IllegalArgumentException("Illegal direction: " + pDirection);
+ }
+ }
+
+ /**
+ * Rotates the image to the given angle. Areas not covered with pixels from
+ * the source image will be left transparent, if possible.
+ *
+ * @param pImage the source image
+ * @param pAngle the angle of rotation, in radians
+ *
+ * @return a new {@code BufferedImage}, unless {@code pAngle == 0.0}
+ */
+ public static BufferedImage createRotated(final Image pImage, final double pAngle) {
+ return createRotated0(toBuffered(pImage), pAngle);
+ }
+
+ private static BufferedImage createRotated0(final BufferedImage pSource, final double pAngle) {
+ if ((Math.abs(Math.toDegrees(pAngle)) % 360) == 0) {
+ return pSource;
+ }
+
+ final boolean fast = ((Math.abs(Math.toDegrees(pAngle)) % 90) == 0.0);
+ final int w = pSource.getWidth();
+ final int h = pSource.getHeight();
+
+ // Compute new width and height
+ double sin = Math.abs(Math.sin(pAngle));
+ double cos = Math.abs(Math.cos(pAngle));
+
+ int newW = (int) Math.floor(w * cos + h * sin);
+ int newH = (int) Math.floor(h * cos + w * sin);
+
+ AffineTransform transform = AffineTransform.getTranslateInstance((newW - w) / 2.0, (newH - h) / 2.0);
+ transform.rotate(pAngle, w / 2.0, h / 2.0);
+ //AffineTransformOp transformOp = new AffineTransformOp(
+ // transform, fast ? AffineTransformOp.TYPE_NEAREST_NEIGHBOR : 3 // 3 == TYPE_BICUBIC
+ //);
+ //
+ //return transformOp.filter(pSource, null);
+
+ // TODO: Figure out if this is correct
+ BufferedImage dest = createTransparent(newW, newH);
+ //ColorModel cm = pSource.getColorModel();
+ //new BufferedImage(cm,
+ // createCompatibleWritableRaster(pSource, cm, newW, newH),
+ // cm.isAlphaPremultiplied(), null);
+
+ // See: http://weblogs.java.net/blog/campbell/archive/2007/03/java_2d_tricker_1.html
+ Graphics2D g = dest.createGraphics();
+ try {
+ g.transform(transform);
+ if (!fast) {
+ // Clear with all transparent
+ //Composite normal = g.getComposite();
+ //g.setComposite(AlphaComposite.Clear);
+ //g.fillRect(0, 0, newW, newH);
+ //g.setComposite(normal);
+
+ // Max quality
+ g.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION,
+ RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
+ g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
+ RenderingHints.VALUE_INTERPOLATION_BILINEAR);
+ g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
+ RenderingHints.VALUE_ANTIALIAS_ON);
+ g.setPaint(new TexturePaint(pSource,
+ new Rectangle2D.Float(0, 0, pSource.getWidth(), pSource.getHeight())));
+ g.fillRect(0, 0, pSource.getWidth(), pSource.getHeight());
+ }
+ else {
+ g.drawImage(pSource, 0, 0, null);
+ }
+ }
+ finally {
+ g.dispose();
+ }
+
+ return dest;
+ }
+
+ /**
+ * Creates a scaled instance of the given {@code Image}, and converts it to
+ * a {@code BufferedImage} if needed.
+ * If the original image is a {@code BufferedImage} the result will have
+ * same type and colormodel. Note that this implies overhead, and is
+ * probably not useful for anything but {@code IndexColorModel} images.
+ *
+ * @param pImage the {@code Image} to scale
+ * @param pWidth width in pixels
+ * @param pHeight height in pixels
+ * @param pHints scaling ints
+ *
+ * @return a {@code BufferedImage}
+ *
+ * @throws NullPointerException if {@code pImage} is {@code null}.
+ *
+ * @see #createResampled(java.awt.Image, int, int, int)
+ * @see Image#getScaledInstance(int,int,int)
+ * @see Image#SCALE_AREA_AVERAGING
+ * @see Image#SCALE_DEFAULT
+ * @see Image#SCALE_FAST
+ * @see Image#SCALE_REPLICATE
+ * @see Image#SCALE_SMOOTH
+ */
+ public static BufferedImage createScaled(Image pImage, int pWidth, int pHeight, int pHints) {
+ ColorModel cm;
+ int type = BI_TYPE_ANY;
+ if (pImage instanceof RenderedImage) {
+ cm = ((RenderedImage) pImage).getColorModel();
+ if (pImage instanceof BufferedImage) {
+ type = ((BufferedImage) pImage).getType();
+ }
+ }
+ else {
+ BufferedImageFactory factory = new BufferedImageFactory(pImage);
+ cm = factory.getColorModel();
+ }
+
+ BufferedImage scaled = createResampled(pImage, pWidth, pHeight, pHints);
+
+ // Convert if colormodels or type differ, to behave as documented
+ if (type != scaled.getType() && type != BI_TYPE_ANY || !equals(scaled.getColorModel(), cm)) {
+ //System.out.print("Converting TYPE " + scaled.getType() + " -> " + type + "... ");
+ //long start = System.currentTimeMillis();
+ WritableRaster raster;
+ if (pImage instanceof BufferedImage) {
+ raster = createCompatibleWritableRaster((BufferedImage) pImage, cm, pWidth, pHeight);
+ }
+ else {
+ raster = cm.createCompatibleWritableRaster(pWidth, pHeight);
+ }
+
+ BufferedImage temp = new BufferedImage(cm, raster, cm.isAlphaPremultiplied(), null);
+
+ if (cm instanceof IndexColorModel && pHints == Image.SCALE_SMOOTH) {
+ new DiffusionDither((IndexColorModel) cm).filter(scaled, temp);
+ }
+ else {
+ drawOnto(temp, scaled);
+ }
+ scaled = temp;
+ //long end = System.currentTimeMillis();
+ //System.out.println("Time: " + (end - start) + " ms");
+ }
+
+ return scaled;
+ }
+
+ private static boolean equals(ColorModel pLeft, ColorModel pRight) {
+ if (pLeft == pRight) {
+ return true;
+ }
+
+ if (!pLeft.equals(pRight)) {
+ return false;
+ }
+
+ // Now, the models are equal, according to the equals method
+ // Test indexcolormodels for equality, the maps must be equal
+ if (pLeft instanceof IndexColorModel) {
+ IndexColorModel icm1 = (IndexColorModel) pLeft;
+ IndexColorModel icm2 = (IndexColorModel) pRight; // NOTE: Safe, they're equal
+
+
+ final int mapSize1 = icm1.getMapSize();
+ final int mapSize2 = icm2.getMapSize();
+
+ if (mapSize1 != mapSize2) {
+ return false;
+ }
+
+ for (int i = 0; i > mapSize1; i++) {
+ if (icm1.getRGB(i) != icm2.getRGB(i)) {
+ return false;
+ }
+ }
+
+ return true;
+
+ }
+
+ return true;
+ }
+
+ /**
+ * Creates a scaled instance of the given {@code Image}, and converts it to
+ * a {@code BufferedImage} if needed.
+ *
+ * @param pImage the {@code Image} to scale
+ * @param pWidth width in pixels
+ * @param pHeight height in pixels
+ * @param pHints scaling mHints
+ *
+ * @return a {@code BufferedImage}
+ *
+ * @throws NullPointerException if {@code pImage} is {@code null}.
+ *
+ * @see Image#SCALE_AREA_AVERAGING
+ * @see Image#SCALE_DEFAULT
+ * @see Image#SCALE_FAST
+ * @see Image#SCALE_REPLICATE
+ * @see Image#SCALE_SMOOTH
+ * @see ResampleOp
+ */
+ public static BufferedImage createResampled(Image pImage, int pWidth, int pHeight, int pHints) {
+ // NOTE: TYPE_4BYTE_ABGR or TYPE_3BYTE_BGR is more efficient when accelerated...
+ BufferedImage image = pImage instanceof BufferedImage
+ ? (BufferedImage) pImage
+ : toBuffered(pImage, BufferedImage.TYPE_4BYTE_ABGR);
+ return createResampled(image, pWidth, pHeight, pHints);
+ }
+
+ /**
+ * Creates a scaled instance of the given {@code RenderedImage}, and
+ * converts it to a {@code BufferedImage} if needed.
+ *
+ * @param pImage the {@code RenderedImage} to scale
+ * @param pWidth width in pixels
+ * @param pHeight height in pixels
+ * @param pHints scaling mHints
+ *
+ * @return a {@code BufferedImage}
+ *
+ * @throws NullPointerException if {@code pImage} is {@code null}.
+ *
+ * @see Image#SCALE_AREA_AVERAGING
+ * @see Image#SCALE_DEFAULT
+ * @see Image#SCALE_FAST
+ * @see Image#SCALE_REPLICATE
+ * @see Image#SCALE_SMOOTH
+ * @see ResampleOp
+ */
+ public static BufferedImage createResampled(RenderedImage pImage, int pWidth, int pHeight, int pHints) {
+ // NOTE: TYPE_4BYTE_ABGR or TYPE_3BYTE_BGR is more efficient when accelerated...
+ BufferedImage image = pImage instanceof BufferedImage
+ ? (BufferedImage) pImage
+ : toBuffered(pImage, pImage.getColorModel().hasAlpha() ? BufferedImage.TYPE_4BYTE_ABGR : BufferedImage.TYPE_3BYTE_BGR);
+ return createResampled(image, pWidth, pHeight, pHints);
+ }
+
+ /**
+ * Creates a scaled instance of the given {@code BufferedImage}.
+ *
+ * @param pImage the {@code BufferedImage} to scale
+ * @param pWidth width in pixels
+ * @param pHeight height in pixels
+ * @param pHints scaling mHints
+ *
+ * @return a {@code BufferedImage}
+ *
+ * @throws NullPointerException if {@code pImage} is {@code null}.
+ *
+ * @see Image#SCALE_AREA_AVERAGING
+ * @see Image#SCALE_DEFAULT
+ * @see Image#SCALE_FAST
+ * @see Image#SCALE_REPLICATE
+ * @see Image#SCALE_SMOOTH
+ * @see ResampleOp
+ */
+ public static BufferedImage createResampled(BufferedImage pImage, int pWidth, int pHeight, int pHints) {
+ // Hints are converted between java.awt.Image hints and filter types
+ return new ResampleOp(pWidth, pHeight, convertAWTHints(pHints)).filter(pImage, null);
+ }
+
+ private static int convertAWTHints(int pHints) {
+ // TODO: These conversions are broken!
+ // box == area average
+ // point == replicate (or..?)
+ switch (pHints) {
+ case Image.SCALE_FAST:
+ case Image.SCALE_REPLICATE:
+ return ResampleOp.FILTER_POINT;
+ case Image.SCALE_AREA_AVERAGING:
+ return ResampleOp.FILTER_BOX;
+ //return ResampleOp.FILTER_CUBIC;
+ case Image.SCALE_SMOOTH:
+ return ResampleOp.FILTER_LANCZOS;
+ default:
+ //return ResampleOp.FILTER_TRIANGLE;
+ return ResampleOp.FILTER_QUADRATIC;
+ }
+ }
+
+ /**
+ * Extracts an {@code IndexColorModel} from the given image.
+ *
+ * @param pImage the image to get the color model from
+ * @param pColors the maximum number of colors in the resulting color model
+ * @param pHints hints controlling transparency and color selection
+ *
+ * @return the extracted {@code IndexColorModel}
+ *
+ * @see #COLOR_SELECTION_DEFAULT
+ * @see #COLOR_SELECTION_FAST
+ * @see #COLOR_SELECTION_QUALITY
+ * @see #TRANSPARENCY_DEFAULT
+ * @see #TRANSPARENCY_OPAQUE
+ * @see #TRANSPARENCY_BITMASK
+ * @see #TRANSPARENCY_TRANSLUCENT
+ */
+ public static IndexColorModel getIndexColorModel(Image pImage, int pColors, int pHints) {
+ return IndexImage.getIndexColorModel(pImage, pColors, pHints);
+ }
+
+ /**
+ * Creates an indexed version of the given image (a {@code BufferedImage}
+ * with an {@code IndexColorModel}.
+ * The resulting image will have a maximum of 256 different colors.
+ * Transparent parts of the original will be replaced with solid black.
+ * Default (possibly HW accelerated) dither will be used.
+ *
+ * @param pImage the image to convert
+ *
+ * @return an indexed version of the given image
+ */
+ public static BufferedImage createIndexed(Image pImage) {
+ return IndexImage.getIndexedImage(toBuffered(pImage), 256, Color.black, IndexImage.DITHER_DEFAULT);
+ }
+
+ /**
+ * Creates an indexed version of the given image (a {@code BufferedImage}
+ * with an {@code IndexColorModel}.
+ *
+ * @param pImage the image to convert
+ * @param pColors number of colors in the resulting image
+ * @param pMatte color to replace transparent parts of the original.
+ * @param pHints hints controlling dither, transparency and color selection
+ *
+ * @return an indexed version of the given image
+ *
+ * @see #COLOR_SELECTION_DEFAULT
+ * @see #COLOR_SELECTION_FAST
+ * @see #COLOR_SELECTION_QUALITY
+ * @see #DITHER_NONE
+ * @see #DITHER_DEFAULT
+ * @see #DITHER_DIFFUSION
+ * @see #DITHER_DIFFUSION_ALTSCANS
+ * @see #TRANSPARENCY_DEFAULT
+ * @see #TRANSPARENCY_OPAQUE
+ * @see #TRANSPARENCY_BITMASK
+ * @see #TRANSPARENCY_TRANSLUCENT
+ */
+ public static BufferedImage createIndexed(Image pImage, int pColors, Color pMatte, int pHints) {
+ return IndexImage.getIndexedImage(toBuffered(pImage), pColors, pMatte, pHints);
+ }
+
+ /**
+ * Creates an indexed version of the given image (a {@code BufferedImage}
+ * with an {@code IndexColorModel}.
+ *
+ * @param pImage the image to convert
+ * @param pColors the {@code IndexColorModel} to be used in the resulting
+ * image.
+ * @param pMatte color to replace transparent parts of the original.
+ * @param pHints hints controlling dither, transparency and color selection
+ *
+ * @return an indexed version of the given image
+ *
+ * @see #COLOR_SELECTION_DEFAULT
+ * @see #COLOR_SELECTION_FAST
+ * @see #COLOR_SELECTION_QUALITY
+ * @see #DITHER_NONE
+ * @see #DITHER_DEFAULT
+ * @see #DITHER_DIFFUSION
+ * @see #DITHER_DIFFUSION_ALTSCANS
+ * @see #TRANSPARENCY_DEFAULT
+ * @see #TRANSPARENCY_OPAQUE
+ * @see #TRANSPARENCY_BITMASK
+ * @see #TRANSPARENCY_TRANSLUCENT
+ */
+ public static BufferedImage createIndexed(Image pImage, IndexColorModel pColors, Color pMatte, int pHints) {
+ return IndexImage.getIndexedImage(toBuffered(pImage), pColors, pMatte, pHints);
+ }
+
+ /**
+ * Creates an indexed version of the given image (a {@code BufferedImage}
+ * with an {@code IndexColorModel}.
+ *
+ * @param pImage the image to convert
+ * @param pColors an {@code Image} used to get colors from. If the image is
+ * has an {@code IndexColorModel}, it will be uesd, otherwise an
+ * {@code IndexColorModel} is created from the image.
+ * @param pMatte color to replace transparent parts of the original.
+ * @param pHints hints controlling dither, transparency and color selection
+ *
+ * @return an indexed version of the given image
+ *
+ * @see #COLOR_SELECTION_DEFAULT
+ * @see #COLOR_SELECTION_FAST
+ * @see #COLOR_SELECTION_QUALITY
+ * @see #DITHER_NONE
+ * @see #DITHER_DEFAULT
+ * @see #DITHER_DIFFUSION
+ * @see #DITHER_DIFFUSION_ALTSCANS
+ * @see #TRANSPARENCY_DEFAULT
+ * @see #TRANSPARENCY_OPAQUE
+ * @see #TRANSPARENCY_BITMASK
+ * @see #TRANSPARENCY_TRANSLUCENT
+ */
+ public static BufferedImage createIndexed(Image pImage, Image pColors, Color pMatte, int pHints) {
+ return IndexImage.getIndexedImage(toBuffered(pImage),
+ IndexImage.getIndexColorModel(pColors, 255, pHints),
+ pMatte, pHints);
+ }
+
+ /**
+ * Sharpens an image using a convolution matrix.
+ * The sharpen kernel used, is defined by the following 3 by 3 matrix:
+ *
+ *
0.0
-0.3
0.0
+ *
-0.3
2.2
-0.3
+ *
0.0
-0.3
0.0
+ *
+ *
+ * This is the same result returned as
+ * {@code sharpen(pOriginal, 0.3f)}.
+ *
+ * @param pOriginal the BufferedImage to sharpen
+ *
+ * @return a new BufferedImage, containing the sharpened image.
+ */
+ public static BufferedImage sharpen(BufferedImage pOriginal) {
+ return convolve(pOriginal, SHARPEN_KERNEL, EDGE_REFLECT);
+ }
+
+ /**
+ * Sharpens an image using a convolution matrix.
+ * The sharpen kernel used, is defined by the following 3 by 3 matrix:
+ *
+ *
0.0
-{@code pAmmount}
0.0
+ *
-{@code pAmmount}
+ *
4.0 * {@code pAmmount} + 1.0
+ *
-{@code pAmmount}
+ *
0.0
-{@code pAmmount}
0.0
+ *
+ *
+ * @param pOriginal the BufferedImage to sharpen
+ * @param pAmmount the ammount of sharpening
+ *
+ * @return a BufferedImage, containing the sharpened image.
+ */
+ public static BufferedImage sharpen(BufferedImage pOriginal, float pAmmount) {
+ if (pAmmount == 0f) {
+ return pOriginal;
+ }
+
+ // Create the convolution matrix
+ float[] data = new float[] {
+ 0.0f, -pAmmount, 0.0f, -pAmmount, 4f * pAmmount + 1f, -pAmmount, 0.0f, -pAmmount, 0.0f
+ };
+
+ // Do the filtering
+ return convolve(pOriginal, new Kernel(3, 3, data), EDGE_REFLECT);
+ }
+
+ /**
+ * Creates a blurred version of the given image.
+ *
+ * @param pOriginal the original image
+ *
+ * @return a new {@code BufferedImage} with a blurred version of the given image
+ */
+ public static BufferedImage blur(BufferedImage pOriginal) {
+ return blur(pOriginal, 1.5f);
+ }
+
+ // Some work to do... Is okay now, for range 0...1, anything above creates
+ // artifacts.
+ // The idea here is that the sum of all terms in the matrix must be 1.
+
+ /**
+ * Creates a blurred version of the given image.
+ *
+ * @param pOriginal the original image
+ * @param pRadius the ammount to blur
+ *
+ * @return a new {@code BufferedImage} with a blurred version of the given image
+ */
+ public static BufferedImage blur(BufferedImage pOriginal, float pRadius) {
+ if (pRadius <= 1f) {
+ return pOriginal;
+ }
+
+ // TODO: Re-implement using two-pass one-dimensional gaussion blur
+ // See: http://en.wikipedia.org/wiki/Gaussian_blur#Implementation
+ // Also see http://www.jhlabs.com/ip/blurring.html
+
+ // TODO: Rethink... Fixed ammount and scale matrix instead?
+// pAmmount = 1f - pAmmount;
+// float pAmmount = 1f - pRadius;
+//
+// // Normalize ammount
+// float normAmt = (1f - pAmmount) / 24;
+//
+// // Create the convolution matrix
+// float[] data = new float[] {
+// normAmt / 2, normAmt, normAmt, normAmt, normAmt / 2,
+// normAmt, normAmt, normAmt * 2, normAmt, normAmt,
+// normAmt, normAmt * 2, pAmmount, normAmt * 2, normAmt,
+// normAmt, normAmt, normAmt * 2, normAmt, normAmt,
+// normAmt / 2, normAmt, normAmt, normAmt, normAmt / 2
+// };
+//
+// // Do the filtering
+// return convolve(pOriginal, new Kernel(5, 5, data), EDGE_REFLECT);
+
+ Kernel horizontal = makeKernel(pRadius);
+ Kernel vertical = new Kernel(horizontal.getHeight(), horizontal.getWidth(), horizontal.getKernelData(null));
+
+ BufferedImage temp = addBorder(pOriginal, horizontal.getWidth() / 2, vertical.getHeight() / 2, EDGE_REFLECT);
+
+ temp = convolve(temp, horizontal, EDGE_NO_OP);
+ temp = convolve(temp, vertical, EDGE_NO_OP);
+
+ return temp.getSubimage(
+ horizontal.getWidth() / 2, vertical.getHeight() / 2, pOriginal.getWidth(), pOriginal.getHeight()
+ );
+ }
+
+ /**
+ * Make a Gaussian blur {@link Kernel}.
+ *
+ * @param radius the blur radius
+ * @return a new blur {@code Kernel}
+ */
+ private static Kernel makeKernel(float radius) {
+ int r = (int) Math.ceil(radius);
+ int rows = r * 2 + 1;
+ float[] matrix = new float[rows];
+ float sigma = radius / 3;
+ float sigma22 = 2 * sigma * sigma;
+ float sigmaPi2 = (float) (2 * Math.PI * sigma);
+ float sqrtSigmaPi2 = (float) Math.sqrt(sigmaPi2);
+ float radius2 = radius * radius;
+ float total = 0;
+ int index = 0;
+ for (int row = -r; row <= r; row++) {
+ float distance = row * row;
+ if (distance > radius2) {
+ matrix[index] = 0;
+ }
+ else {
+ matrix[index] = (float) Math.exp(-(distance) / sigma22) / sqrtSigmaPi2;
+ }
+ total += matrix[index];
+ index++;
+ }
+ for (int i = 0; i < rows; i++) {
+ matrix[i] /= total;
+ }
+
+ return new Kernel(rows, 1, matrix);
+ }
+
+
+ /**
+ * Convolves an image, using a convolution matrix.
+ *
+ * @param pOriginal the BufferedImage to sharpen
+ * @param pKernel the kernel
+ * @param pEdgeOperation the edge operation. Must be one of {@link #EDGE_NO_OP},
+ * {@link #EDGE_ZERO_FILL}, {@link #EDGE_REFLECT} or {@link #EDGE_WRAP}
+ *
+ * @return a new BufferedImage, containing the sharpened image.
+ */
+ public static BufferedImage convolve(BufferedImage pOriginal, Kernel pKernel, int pEdgeOperation) {
+ // Allow for 2 more edge operations
+ BufferedImage original;
+ switch (pEdgeOperation) {
+ case EDGE_REFLECT:
+ case EDGE_WRAP:
+ original = addBorder(pOriginal, pKernel.getWidth() / 2, pKernel.getHeight() / 2, pEdgeOperation);
+ break;
+ default:
+ original = pOriginal;
+ break;
+ }
+
+ // Create convolution operation
+ ConvolveOp convolve = new ConvolveOp(pKernel, pEdgeOperation, null);
+
+ // Workaround for what seems to be a Java2D bug:
+ // ConvolveOp needs explicit destination image type for some "uncommon"
+ // image types. However, TYPE_3BYTE_BGR is what javax.imageio.ImageIO
+ // normally returns for color JPEGs... :-/
+ BufferedImage result = null;
+ if (original.getType() == BufferedImage.TYPE_3BYTE_BGR) {
+ result = createBuffered(
+ pOriginal.getWidth(), pOriginal.getHeight(),
+ pOriginal.getType(), pOriginal.getColorModel().getTransparency()
+ );
+ }
+
+ // Do the filtering (if result is null, a new image will be created)
+ BufferedImage image = convolve.filter(original, result);
+
+ if (pOriginal != original) {
+ // Remove the border
+ image = image.getSubimage(
+ pKernel.getWidth() / 2, pKernel.getHeight() / 2, pOriginal.getWidth(), pOriginal.getHeight()
+ );
+ }
+
+ return image;
+ }
+
+ private static BufferedImage addBorder(final BufferedImage pOriginal, final int pBorderX, final int pBorderY, final int pEdgeOperation) {
+ // TODO: Might be faster if we could clone raster and strech it...
+ int w = pOriginal.getWidth();
+ int h = pOriginal.getHeight();
+
+ ColorModel cm = pOriginal.getColorModel();
+ WritableRaster raster = cm.createCompatibleWritableRaster(w + 2 * pBorderX, h + 2 * pBorderY);
+ BufferedImage bordered = new BufferedImage(cm, raster, cm.isAlphaPremultiplied(), null);
+
+ Graphics2D g = bordered.createGraphics();
+ try {
+ g.setComposite(AlphaComposite.Src);
+ g.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_DISABLE);
+
+ // Draw original in center
+ g.drawImage(pOriginal, pBorderX, pBorderY, null);
+
+ // TODO: I guess we need the top/left etc, if the corner pixels are covered by the kernel
+ switch (pEdgeOperation) {
+ case EDGE_REFLECT:
+ // Top/left (empty)
+ g.drawImage(pOriginal, pBorderX, 0, pBorderX + w, pBorderY, 0, 0, w, 1, null); // Top/center
+ // Top/right (empty)
+
+ g.drawImage(pOriginal, -w + pBorderX, pBorderY, pBorderX, h + pBorderY, 0, 0, 1, h, null); // Center/left
+ // Center/center (already drawn)
+ g.drawImage(pOriginal, w + pBorderX, pBorderY, 2 * pBorderX + w, h + pBorderY, w - 1, 0, w, h, null); // Center/right
+
+ // Bottom/left (empty)
+ g.drawImage(pOriginal, pBorderX, pBorderY + h, pBorderX + w, 2 * pBorderY + h, 0, h - 1, w, h, null); // Bottom/center
+ // Bottom/right (empty)
+ break;
+ case EDGE_WRAP:
+ g.drawImage(pOriginal, -w + pBorderX, -h + pBorderY, null); // Top/left
+ g.drawImage(pOriginal, pBorderX, -h + pBorderY, null); // Top/center
+ g.drawImage(pOriginal, w + pBorderX, -h + pBorderY, null); // Top/right
+
+ g.drawImage(pOriginal, -w + pBorderX, pBorderY, null); // Center/left
+ // Center/center (already drawn)
+ g.drawImage(pOriginal, w + pBorderX, pBorderY, null); // Center/right
+
+ g.drawImage(pOriginal, -w + pBorderX, h + pBorderY, null); // Bottom/left
+ g.drawImage(pOriginal, pBorderX, h + pBorderY, null); // Bottom/center
+ g.drawImage(pOriginal, w + pBorderX, h + pBorderY, null); // Bottom/right
+ break;
+ default:
+ throw new IllegalArgumentException("Illegal edge operation " + pEdgeOperation);
+ }
+
+ }
+ finally {
+ g.dispose();
+ }
+
+ //ConvolveTester.showIt(bordered, "jaffe");
+
+ return bordered;
+ }
+
+ /**
+ * Adds contrast
+ *
+ * @param pOriginal the BufferedImage to add contrast to
+ *
+ * @return an {@code Image}, containing the contrasted image.
+ */
+ public static Image contrast(Image pOriginal) {
+ return contrast(pOriginal, 0.3f);
+ }
+
+ /**
+ * Changes the contrast of the image
+ *
+ * @param pOriginal the {@code Image} to change
+ * @param pAmmount the ammount of contrast in the range [-1.0..1.0].
+ *
+ * @return an {@code Image}, containing the contrasted image.
+ */
+ public static Image contrast(Image pOriginal, float pAmmount) {
+ // No change, return original
+ if (pAmmount == 0f) {
+ return pOriginal;
+ }
+
+ // Create filter
+ RGBImageFilter filter = new BrightnessContrastFilter(0f, pAmmount);
+
+ // Return contrast adjusted image
+ return filter(pOriginal, filter);
+ }
+
+
+ /**
+ * Changes the brightness of the original image.
+ *
+ * @param pOriginal the {@code Image} to change
+ * @param pAmmount the ammount of brightness in the range [-2.0..2.0].
+ *
+ * @return an {@code Image}
+ */
+ public static Image brightness(Image pOriginal, float pAmmount) {
+ // No change, return original
+ if (pAmmount == 0f) {
+ return pOriginal;
+ }
+
+ // Create filter
+ RGBImageFilter filter = new BrightnessContrastFilter(pAmmount, 0f);
+
+ // Return brightness adjusted image
+ return filter(pOriginal, filter);
+ }
+
+
+ /**
+ * Converts an image to grayscale.
+ *
+ * @see GrayFilter
+ * @see RGBImageFilter
+ *
+ * @param pOriginal the image to convert.
+ * @return a new Image, containing the gray image data.
+ */
+ public static Image grayscale(Image pOriginal) {
+ // Create filter
+ RGBImageFilter filter = new GrayFilter();
+
+ // Convert to gray
+ return filter(pOriginal, filter);
+ }
+
+ /**
+ * Filters an image, using the given {@code ImageFilter}.
+ *
+ * @param pOriginal the original image
+ * @param pFilter the filter to apply
+ *
+ * @return the new {@code Image}
+ */
+ public static Image filter(Image pOriginal, ImageFilter pFilter) {
+ // Create a filtered source
+ ImageProducer source = new FilteredImageSource(pOriginal.getSource(), pFilter);
+
+ // Create new image
+ return Toolkit.getDefaultToolkit().createImage(source);
+ }
+
+ /**
+ * Tries to use H/W-accellerated code for an image for display purposes.
+ * Note that transparent parts of the image might be replaced by solid
+ * color. Additional image information not used by the current diplay
+ * hardware may be discarded, like extra bith depth etc.
+ *
+ * @param pImage any {@code Image}
+ * @return a {@code BufferedImage}
+ */
+ public static BufferedImage accelerate(Image pImage) {
+ return accelerate(pImage, null, DEFAULT_CONFIGURATION);
+ }
+
+ /**
+ * Tries to use H/W-accellerated code for an image for display purposes.
+ * Note that transparent parts of the image might be replaced by solid
+ * color. Additional image information not used by the current diplay
+ * hardware may be discarded, like extra bith depth etc.
+ *
+ * @param pImage any {@code Image}
+ * @param pConfiguration the {@code GraphicsConfiguration} to accelerate
+ * for
+ *
+ * @return a {@code BufferedImage}
+ */
+ public static BufferedImage accelerate(Image pImage, GraphicsConfiguration pConfiguration) {
+ return accelerate(pImage, null, pConfiguration);
+ }
+
+ /**
+ * Tries to use H/W-accellerated code for an image for display purposes.
+ * Note that transparent parts of the image will be replaced by solid
+ * color. Additional image information not used by the current diplay
+ * hardware may be discarded, like extra bith depth etc.
+ *
+ * @param pImage any {@code Image}
+ * @param pBackgroundColor the background color to replace any transparent
+ * parts of the image.
+ * May be {@code null}, in such case the color is undefined.
+ * @param pConfiguration the graphics configuration
+ * May be {@code null}, in such case the color is undefined.
+ *
+ * @return a {@code BufferedImage}
+ */
+ static BufferedImage accelerate(Image pImage, Color pBackgroundColor, GraphicsConfiguration pConfiguration) {
+ // Skip acceleration if the layout of the image and color model is already ok
+ if (pImage instanceof BufferedImage) {
+ BufferedImage buffered = (BufferedImage) pImage;
+ // TODO: What if the createCompatibleImage insist on TYPE_CUSTOM...? :-P
+ if (buffered.getType() != BufferedImage.TYPE_CUSTOM && equals(buffered.getColorModel(), pConfiguration.getColorModel(buffered.getTransparency()))) {
+ return buffered;
+ }
+ }
+ if (pImage == null) {
+ throw new IllegalArgumentException("image == null");
+ }
+
+ int w = ImageUtil.getWidth(pImage);
+ int h = ImageUtil.getHeight(pImage);
+
+ // Create accelerated version
+ BufferedImage temp = createClear(w, h, BI_TYPE_ANY, getTransparency(pImage), pBackgroundColor, pConfiguration);
+ drawOnto(temp, pImage);
+
+ return temp;
+ }
+
+ private static int getTransparency(Image pImage) {
+ if (pImage instanceof BufferedImage) {
+ BufferedImage bi = (BufferedImage) pImage;
+ return bi.getTransparency();
+ }
+ return Transparency.OPAQUE;
+ }
+
+ /**
+ * Creates a transparent image.
+ *
+ * @param pWidth the requested width of the image
+ * @param pHeight the requested height of the image
+ *
+ * @throws IllegalArgumentException if {@code pType} is not a valid type
+ * for {@code BufferedImage}
+ *
+ * @return the new image
+ */
+ public static BufferedImage createTransparent(int pWidth, int pHeight) {
+ return createTransparent(pWidth, pHeight, BI_TYPE_ANY);
+ }
+
+ /**
+ * Creates a transparent image.
+ *
+ * @see BufferedImage#BufferedImage(int,int,int)
+ *
+ * @param pWidth the requested width of the image
+ * @param pHeight the requested height of the image
+ * @param pType the type of {@code BufferedImage} to create
+ *
+ * @throws IllegalArgumentException if {@code pType} is not a valid type
+ * for {@code BufferedImage}
+ *
+ * @return the new image
+ */
+ public static BufferedImage createTransparent(int pWidth, int pHeight, int pType) {
+ // Create
+ BufferedImage image = createBuffered(pWidth, pHeight, pType, Transparency.TRANSLUCENT);
+
+ // Clear image with transparent alpha by drawing a rectangle
+ Graphics2D g = image.createGraphics();
+ try {
+ g.setComposite(AlphaComposite.Clear);
+ g.fillRect(0, 0, pWidth, pHeight);
+ }
+ finally {
+ g.dispose();
+ }
+
+ return image;
+ }
+
+ /**
+ * Creates a clear image with the given background color.
+ *
+ * @see BufferedImage#BufferedImage(int,int,int)
+ *
+ * @param pWidth the requested width of the image
+ * @param pHeight the requested height of the image
+ * @param pBackground the background color. The color may be translucent.
+ * May be {@code null}, in such case the color is undefined.
+ *
+ * @throws IllegalArgumentException if {@code pType} is not a valid type
+ * for {@code BufferedImage}
+ *
+ * @return the new image
+ */
+ public static BufferedImage createClear(int pWidth, int pHeight, Color pBackground) {
+ return createClear(pWidth, pHeight, BI_TYPE_ANY, pBackground);
+ }
+
+ /**
+ * Creates a clear image with the given background color.
+ *
+ * @see BufferedImage#BufferedImage(int,int,int)
+ *
+ * @param pWidth the width of the image to create
+ * @param pHeight the height of the image to create
+ * @param pType the type of image to create (one of the constants from
+ * {@link BufferedImage} or {@link #BI_TYPE_ANY})
+ * @param pBackground the background color. The color may be translucent.
+ * May be {@code null}, in such case the color is undefined.
+ *
+ * @throws IllegalArgumentException if {@code pType} is not a valid type
+ * for {@code BufferedImage}
+ *
+ * @return the new image
+ */
+ public static BufferedImage createClear(int pWidth, int pHeight, int pType, Color pBackground) {
+ return createClear(pWidth, pHeight, pType, Transparency.OPAQUE, pBackground, DEFAULT_CONFIGURATION);
+ }
+
+ static BufferedImage createClear(int pWidth, int pHeight, int pType, int pTransparency, Color pBackground, GraphicsConfiguration pConfiguration) {
+ // Create
+ int transparency = (pBackground != null) ? pBackground.getTransparency() : pTransparency;
+ BufferedImage image = createBuffered(pWidth, pHeight, pType, transparency, pConfiguration);
+
+ if (pBackground != null) {
+ // Clear image with clear color, by drawing a rectangle
+ Graphics2D g = image.createGraphics();
+ try {
+ g.setComposite(AlphaComposite.Src); // Allow color to be translucent
+ g.setColor(pBackground);
+ g.fillRect(0, 0, pWidth, pHeight);
+ }
+ finally {
+ g.dispose();
+ }
+ }
+
+ return image;
+ }
+
+ /**
+ * Creates a {@code BufferedImage} of the given size and type. If possible,
+ * uses accelerated versions of BufferedImage from GraphicsConfiguration.
+ *
+ * @param pWidth the width of the image to create
+ * @param pHeight the height of the image to create
+ * @param pType the type of image to create (one of the constants from
+ * {@link BufferedImage} or {@link #BI_TYPE_ANY})
+ * @param pTransparency the transparency type (from {@link Transparency})
+ *
+ * @return a {@code BufferedImage}
+ */
+ private static BufferedImage createBuffered(int pWidth, int pHeight, int pType, int pTransparency) {
+ return createBuffered(pWidth, pHeight, pType, pTransparency, DEFAULT_CONFIGURATION);
+ }
+
+ static BufferedImage createBuffered(int pWidth, int pHeight, int pType, int pTransparency,
+ GraphicsConfiguration pConfiguration) {
+ if (VM_SUPPORTS_ACCELERATION && pType == BI_TYPE_ANY) {
+ GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment();
+ if (supportsAcceleration(env)) {
+ return getConfiguration(pConfiguration).createCompatibleImage(pWidth, pHeight, pTransparency);
+ }
+ }
+
+ return new BufferedImage(pWidth, pHeight, getImageType(pType, pTransparency));
+ }
+
+ private static GraphicsConfiguration getConfiguration(final GraphicsConfiguration pConfiguration) {
+ return pConfiguration != null ? pConfiguration : DEFAULT_CONFIGURATION;
+ }
+
+ private static int getImageType(int pType, int pTransparency) {
+ // TODO: Handle TYPE_CUSTOM?
+ if (pType != BI_TYPE_ANY) {
+ return pType;
+ }
+ else {
+ switch (pTransparency) {
+ case Transparency.OPAQUE:
+ return BufferedImage.TYPE_INT_RGB;
+ case Transparency.BITMASK:
+ case Transparency.TRANSLUCENT:
+ return BufferedImage.TYPE_INT_ARGB;
+ default:
+ throw new IllegalArgumentException("Unknown transparency type: " + pTransparency);
+ }
+ }
+ }
+
+ /**
+ * Tests if the given {@code GraphicsEnvironment} supports accelleration
+ *
+ * @param pEnv the environment
+ * @return {@code true} if the {@code GraphicsEnvironment} supports
+ * acceleration
+ */
+ private static boolean supportsAcceleration(GraphicsEnvironment pEnv) {
+ try {
+ // Acceleration only supported in non-headless environments, on 1.4+ VMs
+ return /*VM_SUPPORTS_ACCELERATION &&*/ !pEnv.isHeadlessInstance();
+ }
+ catch (LinkageError ignore) {
+ // Means we are not in a 1.4+ VM, so skip testing for headless again
+ VM_SUPPORTS_ACCELERATION = false;
+ }
+
+ // If the invocation fails, assume no accelleration is possible
+ return false;
+ }
+
+ /**
+ * Gets the width of an Image.
+ * This method has the side-effect of completely loading the image.
+ *
+ * @param pImage an image.
+ *
+ * @return the width of the image, or -1 if the width could not be
+ * determined (i.e. an error occured while waiting for the
+ * image to load).
+ */
+ public static int getWidth(Image pImage) {
+ int width = pImage.getWidth(NULL_COMPONENT);
+ if (width < 0) {
+ if (!waitForImage(pImage)) {
+ return -1; // Error while waiting
+ }
+ width = pImage.getWidth(NULL_COMPONENT);
+ }
+
+ return width;
+ }
+
+ /**
+ * Gets the height of an Image.
+ * This method has the side-effect of completely loading the image.
+ *
+ * @param pImage an image.
+ *
+ * @return the height of the image, or -1 if the height could not be
+ * determined (i.e. an error occured while waiting for the
+ * image to load).
+ */
+ public static int getHeight(Image pImage) {
+ int height = pImage.getHeight(NULL_COMPONENT);
+ if (height < 0) {
+ if (!waitForImage(pImage)) {
+ return -1; // Error while waiting
+ }
+ height = pImage.getHeight(NULL_COMPONENT);
+ }
+
+ return height;
+ }
+
+ /**
+ * Waits for an image to load completely.
+ * Will wait forever.
+ *
+ * @param pImage an Image object to wait for.
+ *
+ * @return true if the image was loaded successfully, false if an error
+ * occured, or the wait was interrupted.
+ *
+ * @see #waitForImage(Image,long)
+ */
+ public static boolean waitForImage(Image pImage) {
+ return waitForImages(new Image[]{pImage}, -1L);
+ }
+
+ /**
+ * Waits for an image to load completely.
+ * Will wait the specified time.
+ *
+ * @param pImage an Image object to wait for.
+ * @param pTimeOut the time to wait, in milliseconds.
+ *
+ * @return true if the image was loaded successfully, false if an error
+ * occured, or the wait was interrupted.
+ *
+ * @see #waitForImages(Image[],long)
+ */
+ public static boolean waitForImage(Image pImage, long pTimeOut) {
+ return waitForImages(new Image[]{pImage}, pTimeOut);
+ }
+
+ /**
+ * Waits for a number of images to load completely.
+ * Will wait forever.
+ *
+ * @param pImages an array of Image objects to wait for.
+ *
+ * @return true if the images was loaded successfully, false if an error
+ * occured, or the wait was interrupted.
+ *
+ * @see #waitForImages(Image[],long)
+ */
+ public static boolean waitForImages(Image[] pImages) {
+ return waitForImages(pImages, -1L);
+ }
+
+ /**
+ * Waits for a number of images to load completely.
+ * Will wait the specified time.
+ *
+ * @param pImages an array of Image objects to wait for
+ * @param pTimeOut the time to wait, in milliseconds
+ *
+ * @return true if the images was loaded successfully, false if an error
+ * occured, or the wait was interrupted.
+ */
+ public static boolean waitForImages(Image[] pImages, long pTimeOut) {
+ // TODO: Need to make sure that we don't wait for the same image many times
+ // Use hashcode as id? Don't remove images from tracker? Hmmm...
+ boolean success = true;
+
+ // Create a local id for use with the mediatracker
+ int imageId;
+
+ // NOTE: The synchronization throws IllegalMonitorStateException if
+ // using JIT on J2SE 1.2 (tested version Sun JRE 1.2.2_017).
+ // Works perfectly interpreted... Hmmm...
+ //synchronized (sTrackerMutex) {
+ //imageId = ++sTrackerId;
+ //}
+
+ // NOTE: This is very experimental...
+ imageId = pImages.length == 1 ? System.identityHashCode(pImages[0]) : System.identityHashCode(pImages);
+
+ // Add images to tracker
+ for (Image image : pImages) {
+ sTracker.addImage(image, imageId);
+
+ // Start loading immediately
+ if (sTracker.checkID(imageId, false)) {
+ // Image is done, so remove again
+ sTracker.removeImage(image, imageId);
+ }
+ }
+
+ try {
+ if (pTimeOut < 0L) {
+ // Just wait
+ sTracker.waitForID(imageId);
+ }
+ else {
+ // Wait until timeout
+ // NOTE: waitForID(int, long) return value is undocumented.
+ // I assume that it returns true, if the image(s) loaded
+ // successfully before the timeout, however, I always check
+ // isErrorID later on, just in case...
+ success = sTracker.waitForID(imageId, pTimeOut);
+ }
+ }
+ catch (InterruptedException ie) {
+ // Interrupted while waiting, image not loaded
+ success = false;
+ }
+ finally {
+ // Remove images from mediatracker
+ for (Image pImage : pImages) {
+ sTracker.removeImage(pImage, imageId);
+ }
+ }
+
+ // If the wait was successfull, and no errors were reported for the
+ // images, return true
+ return success && !sTracker.isErrorID(imageId);
+ }
+
+ /**
+ * Tests wether the image has any transparent or semi-transparent pixels.
+ *
+ * @param pImage the image
+ * @param pFast if {@code true}, the method tests maximum 10 x 10 pixels,
+ * evenly spaced out in the image.
+ *
+ * @return {@code true} if transparent pixels are found, otherwise
+ * {@code false}.
+ */
+ public static boolean hasTransparentPixels(RenderedImage pImage, boolean pFast) {
+ if (pImage == null) {
+ return false;
+ }
+
+ // First, test if the ColorModel supports alpha...
+ ColorModel cm = pImage.getColorModel();
+ if (!cm.hasAlpha()) {
+ return false;
+ }
+
+ if (cm.getTransparency() != Transparency.BITMASK
+ && cm.getTransparency() != Transparency.TRANSLUCENT) {
+ return false;
+ }
+
+ // ... if so, test the pixels of the image hard way
+ Object data = null;
+
+ // Loop over tiles (noramally, BufferedImages have only one)
+ for (int yT = pImage.getMinTileY(); yT < pImage.getNumYTiles(); yT++) {
+ for (int xT = pImage.getMinTileX(); xT < pImage.getNumXTiles(); xT++) {
+ // Test pixels of each tile
+ Raster raster = pImage.getTile(xT, yT);
+ int xIncrement = pFast ? Math.max(raster.getWidth() / 10, 1) : 1;
+ int yIncrement = pFast ? Math.max(raster.getHeight() / 10, 1) : 1;
+
+ for (int y = 0; y < raster.getHeight(); y += yIncrement) {
+ for (int x = 0; x < raster.getWidth(); x += xIncrement) {
+ // Copy data for each pixel, without allocation array
+ data = raster.getDataElements(x, y, data);
+
+ // Test alpha value
+ if (cm.getAlpha(data) != 0xff) {
+ return true;
+ }
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Creates a translucent version of the given color.
+ *
+ * @param pColor the original color
+ * @param pTransparency the transparency level ({@code 0 - 255})
+ * @return a translucent color
+ *
+ * @throws NullPointerException if {@code pColor} is {@code null}
+ */
+ public static Color createTranslucent(Color pColor, int pTransparency) {
+ //return new Color(pColor.getRed(), pColor.getGreen(), pColor.getBlue(), pTransparency);
+ return new Color(((pTransparency & 0xff) << 24) | (pColor.getRGB() & 0x00ffffff), true);
+ }
+
+ /**
+ * Blends two ARGB values half and half, to create a tone inbetween.
+ *
+ * @param pRGB1 color 1
+ * @param pRGB2 color 2
+ * @return the new rgb value
+ */
+ static int blend(int pRGB1, int pRGB2) {
+ // Slightly modified from http://www.compuphase.com/graphic/scale3.htm
+ // to support alpha values
+ return (((pRGB1 ^ pRGB2) & 0xfefefefe) >> 1) + (pRGB1 & pRGB2);
+ }
+
+ /**
+ * Blends two colors half and half, to create a tone inbetween.
+ *
+ * @param pColor color 1
+ * @param pOther color 2
+ * @return a new {@code Color}
+ */
+ public static Color blend(Color pColor, Color pOther) {
+ return new Color(blend(pColor.getRGB(), pOther.getRGB()), true);
+
+ /*
+ return new Color((pColor.getRed() + pOther.getRed()) / 2,
+ (pColor.getGreen() + pOther.getGreen()) / 2,
+ (pColor.getBlue() + pOther.getBlue()) / 2,
+ (pColor.getAlpha() + pOther.getAlpha()) / 2);
+ */
+ }
+
+ /**
+ * Blends two colors, controlled by the blendfactor.
+ * A factor of {@code 0.0} will return the first color,
+ * a factor of {@code 1.0} will return the second.
+ *
+ * @param pColor color 1
+ * @param pOther color 2
+ * @param pBlendFactor {@code [0...1]}
+ * @return a new {@code Color}
+ */
+ public static Color blend(Color pColor, Color pOther, float pBlendFactor) {
+ float inverseBlend = (1f - pBlendFactor);
+ return new Color(
+ clamp((pColor.getRed() * inverseBlend) + (pOther.getRed() * pBlendFactor)),
+ clamp((pColor.getGreen() * inverseBlend) + (pOther.getGreen() * pBlendFactor)),
+ clamp((pColor.getBlue() * inverseBlend) + (pOther.getBlue() * pBlendFactor)),
+ clamp((pColor.getAlpha() * inverseBlend) + (pOther.getAlpha() * pBlendFactor))
+ );
+ }
+
+ private static int clamp(float f) {
+ return (int) f;
+ }
+ /**
+ * PixelGrabber subclass that stores any potential properties from an image.
+ */
+ /*
+ private static class MyPixelGrabber extends PixelGrabber {
+ private Hashtable mProps = null;
+
+ public MyPixelGrabber(Image pImage) {
+ // Simply grab all pixels, do not convert to default RGB space
+ super(pImage, 0, 0, -1, -1, false);
+ }
+
+ // Default implementation does not store the properties...
+ public void setProperties(Hashtable pProps) {
+ super.setProperties(pProps);
+ mProps = pProps;
+ }
+
+ public Hashtable getProperties() {
+ return mProps;
+ }
+ }
+ */
+
+ /**
+ * Gets the transfer type from the given {@code ColorModel}.
+ *
+ * NOTE: This is a workaround for missing functionality in JDK 1.2.
+ *
+ * @param pModel the color model
+ * @return the transfer type
+ *
+ * @throws NullPointerException if {@code pModel} is {@code null}.
+ *
+ * @see java.awt.image.ColorModel#getTransferType()
+ */
+ public static int getTransferType(ColorModel pModel) {
+ if (COLORMODEL_TRANSFERTYPE_SUPPORTED) {
+ return pModel.getTransferType();
+ }
+ else {
+ // Stupid workaround
+ // TODO: Create something that performs better
+ return pModel.createCompatibleSampleModel(1, 1).getDataType();
+ }
+ }
+}
\ No newline at end of file
diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/IndexImage.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/IndexImage.java
new file mode 100755
index 00000000..950b195a
--- /dev/null
+++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/IndexImage.java
@@ -0,0 +1,1530 @@
+/*
+ * Copyright (c) 2008, Harald Kuhr
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * Neither the name "TwelveMonkeys" nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+/*
+******************************************************************************
+*
+* ============================================================================
+* The Apache Software License, Version 1.1
+* ============================================================================
+*
+* Copyright (C) 2000 The Apache Software Foundation. All rights reserved.
+*
+* Redistribution and use in source and binary forms, with or without modifica-
+* tion, are permitted provided that the following conditions are met:
+*
+* 1. Redistributions of source code must retain the above copyright notice,
+* this list of conditions and the following disclaimer.
+*
+* 2. Redistributions in binary form must reproduce the above copyright notice,
+* this list of conditions and the following disclaimer in the documentation
+* and/or other materials provided with the distribution.
+*
+* 3. The end-user documentation included with the redistribution, if any, must
+* include the following acknowledgment: "This product includes software
+* developed by the Apache Software Foundation (http://www.apache.org/)."
+* Alternately, this acknowledgment may appear in the software itself, if
+* and wherever such third-party acknowledgments normally appear.
+*
+* 4. The names "Batik" and "Apache Software Foundation" must not be used to
+* endorse or promote products derived from this software without prior
+* written permission. For written permission, please contact
+* apache@apache.org.
+*
+* 5. Products derived from this software may not be called "Apache", nor may
+* "Apache" appear in their name, without prior written permission of the
+* Apache Software Foundation.
+*
+* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
+* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+* APACHE SOFTWARE FOUNDATION OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLU-
+* DING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
+* OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*
+* This software consists of voluntary contributions made by many individuals
+* on behalf of the Apache Software Foundation. For more information on the
+* Apache Software Foundation, please see .
+*
+******************************************************************************
+*
+*/
+
+package com.twelvemonkeys.image;
+
+import com.twelvemonkeys.io.FileUtil;
+import com.twelvemonkeys.lang.StringUtil;
+
+import javax.imageio.ImageIO;
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.awt.image.ColorModel;
+import java.awt.image.IndexColorModel;
+import java.awt.image.RenderedImage;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * This class implements an adaptive pallete generator to reduce images
+ * to a variable number of colors.
+ * It can also render images into fixed color pallettes.
+ *
+ * Support for the default JVM (ordered/pattern) dither, Floyd-Steinberg like
+ * error-diffusion and no dither, controlled by the hints
+ * {@link #DITHER_DIFFUSION},
+ * {@link #DITHER_NONE} and
+ * {@link #DITHER_DEFAULT}.
+ *
+ * Color selection speed/accuracy can be controlled using the hints
+ * {@link #COLOR_SELECTION_FAST},
+ * {@link #COLOR_SELECTION_QUALITY} and
+ * {@link #COLOR_SELECTION_DEFAULT}.
+ *
+ * Transparency support can be controlled using the hints
+ * {@link #TRANSPARENCY_OPAQUE},
+ * {@link #TRANSPARENCY_BITMASK} and
+ * {@link #TRANSPARENCY_TRANSLUCENT}.
+ *
+ *
+ *
+ *
+ * This product includes software developed by the Apache Software Foundation.
+ *
+ * This software consists of voluntary contributions made by many individuals
+ * on behalf of the Apache Software Foundation. For more information on the
+ * Apache Software Foundation, please see http://www.apache.org/
+ *
+ *
+ * @author Thomas DeWeese
+ * @author Jun Inamori
+ * @author Harald Kuhr
+ * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/IndexImage.java#1 $
+ * @see DiffusionDither
+ */
+class IndexImage {
+
+ /**
+ * Dither mask
+ */
+ protected final static int DITHER_MASK = 0xFF;
+
+ /**
+ * Java default dither
+ */
+ public final static int DITHER_DEFAULT = 0x00;
+
+ /**
+ * No dither
+ */
+ public final static int DITHER_NONE = 0x01;
+
+ /**
+ * Error diffusion dither
+ */
+ public final static int DITHER_DIFFUSION = 0x02;
+
+ /**
+ * Error diffusion dither with alternating scans
+ */
+ public final static int DITHER_DIFFUSION_ALTSCANS = 0x03;
+
+ /**
+ * Color Selection mask
+ */
+ protected final static int COLOR_SELECTION_MASK = 0xFF00;
+
+ /**
+ * Default color selection
+ */
+ public final static int COLOR_SELECTION_DEFAULT = 0x0000;
+
+ /**
+ * Prioritize speed
+ */
+ public final static int COLOR_SELECTION_FAST = 0x0100;
+
+ /**
+ * Prioritize quality
+ */
+ public final static int COLOR_SELECTION_QUALITY = 0x0200;
+
+ /**
+ * Transparency mask
+ */
+ protected final static int TRANSPARENCY_MASK = 0xFF0000;
+
+ /**
+ * Default transparency (none)
+ */
+ public final static int TRANSPARENCY_DEFAULT = 0x000000;
+
+ /**
+ * Discard any alpha information
+ */
+ public final static int TRANSPARENCY_OPAQUE = 0x010000;
+
+ /**
+ * Convert alpha to bitmask
+ */
+ public final static int TRANSPARENCY_BITMASK = 0x020000;
+
+ /**
+ * Keep original alpha (not supported yet)
+ */
+ protected final static int TRANSPARENCY_TRANSLUCENT = 0x030000;
+
+ /**
+ * Used to track a color and the number of pixels of that colors
+ */
+ private static class Counter {
+
+ /**
+ * Field val
+ */
+ public int val;
+
+ /**
+ * Field count
+ */
+ public int count = 1;
+
+ /**
+ * Constructor Counter
+ *
+ * @param val the initial value
+ */
+ public Counter(int val) {
+ this.val = val;
+ }
+
+ /**
+ * Method add
+ *
+ * @param val the new value
+ * @return {@code true} if the value was added, otherwise {@code false}
+ */
+ public boolean add(int val) {
+ // See if the value matches us...
+ if (this.val != val) {
+ return false;
+ }
+ count++;
+ return true;
+ }
+ }
+
+ /**
+ * Used to define a cube of the colorspace. The cube can be split
+ * approximagely in half to generate two cubes.
+ */
+ private static class Cube {
+ int[] min = {0, 0, 0}, max = {255, 255, 255};
+ boolean done = false;
+ List[] colors = null;
+ int count = 0;
+ static final int RED = 0;
+ static final int GRN = 1;
+ static final int BLU = 2;
+
+ /**
+ * Define a new cube.
+ *
+ * @param colors contains the 3D color histogram to be subdivided
+ * @param count the total number of pixels in the 3D histogram.
+ */
+ public Cube(List[] colors, int count) {
+ this.colors = colors;
+ this.count = count;
+ }
+
+ /**
+ * If this returns true then the cube can not be subdivided any
+ * further
+ *
+ * @return true if cube can not be subdivided any further
+ */
+ public boolean isDone() {
+ return done;
+ }
+
+ /**
+ * Splits the cube into two parts. This cube is
+ * changed to be one half and the returned cube is the other half.
+ * This tries to pick the right channel to split on.
+ *
+ * @return the {@code Cube} containing the other half
+ */
+ public Cube split() {
+ int dr = max[0] - min[0] + 1;
+ int dg = max[1] - min[1] + 1;
+ int db = max[2] - min[2] + 1;
+ int c0, c1, splitChannel;
+
+ // Figure out which axis is the longest and split along
+ // that axis (this tries to keep cubes square-ish).
+ if (dr >= dg) {
+ c0 = GRN;
+ if (dr >= db) {
+ splitChannel = RED;
+ c1 = BLU;
+ }
+ else {
+ splitChannel = BLU;
+ c1 = RED;
+ }
+ }
+ else if (dg >= db) {
+ splitChannel = GRN;
+ c0 = RED;
+ c1 = BLU;
+ }
+ else {
+ splitChannel = BLU;
+ c0 = RED;
+ c1 = GRN;
+ }
+ Cube ret;
+
+ ret = splitChannel(splitChannel, c0, c1);
+ if (ret != null) {
+ return ret;
+ }
+ ret = splitChannel(c0, splitChannel, c1);
+ if (ret != null) {
+ return ret;
+ }
+ ret = splitChannel(c1, splitChannel, c0);
+ if (ret != null) {
+ return ret;
+ }
+ done = true;
+
+ return null;
+ }
+
+ /**
+ * Splits the image according to the parameters. It tries
+ * to find a location where half the pixels are on one side
+ * and half the pixels are on the other.
+ *
+ * @param splitChannel split channel
+ * @param c0 channel 0
+ * @param c1 channel 1
+ * @return the {@code Cube} containing the other half
+ */
+ public Cube splitChannel(int splitChannel, int c0, int c1) {
+ if (min[splitChannel] == max[splitChannel]) {
+ return null;
+ }
+ int splitSh4 = (2 - splitChannel) * 4;
+ int c0Sh4 = (2 - c0) * 4;
+ int c1Sh4 = (2 - c1) * 4;
+
+ // int splitSh8 = (2-splitChannel)*8;
+ // int c0Sh8 = (2-c0)*8;
+ // int c1Sh8 = (2-c1)*8;
+ //
+ int half = count / 2;
+
+ // Each entry is the number of pixels that have that value
+ // in the split channel within the cube (so pixels
+ // that have that value in the split channel aren't counted
+ // if they are outside the cube in the other color channels.
+ int counts[] = new int[256];
+ int tcount = 0;
+
+ // System.out.println("Cube: [" +
+ // min[0] + "-" + max[0] + "] [" +
+ // min[1] + "-" + max[1] + "] [" +
+ // min[2] + "-" + max[2] + "]");
+ int[] minIdx = {min[0] >> 4, min[1] >> 4, min[2] >> 4};
+ int[] maxIdx = {max[0] >> 4, max[1] >> 4, max[2] >> 4};
+ int minR = min[0], minG = min[1], minB = min[2];
+ int maxR = max[0], maxG = max[1], maxB = max[2];
+ int val;
+ int[] vals = {0, 0, 0};
+
+ for (int i = minIdx[splitChannel]; i <= maxIdx[splitChannel]; i++) {
+ int idx1 = i << splitSh4;
+
+ for (int j = minIdx[c0]; j <= maxIdx[c0]; j++) {
+ int idx2 = idx1 | (j << c0Sh4);
+
+ for (int k = minIdx[c1]; k <= maxIdx[c1]; k++) {
+ int idx = idx2 | (k << c1Sh4);
+ List v = colors[idx];
+
+ if (v == null) {
+ continue;
+ }
+ Iterator itr = v.iterator();
+ Counter c;
+
+ while (itr.hasNext()) {
+ c = (Counter) itr.next();
+ val = c.val;
+ vals[0] = (val & 0xFF0000) >> 16;
+ vals[1] = (val & 0xFF00) >> 8;
+ vals[2] = (val & 0xFF);
+ if (((vals[0] >= minR) && (vals[0] <= maxR)) && ((vals[1] >= minG) && (vals[1] <= maxG))
+ && ((vals[2] >= minB) && (vals[2] <= maxB))) {
+
+ // The val lies within this cube so count it.
+ counts[vals[splitChannel]] += c.count;
+ tcount += c.count;
+ }
+ }
+ }
+ }
+
+ // We've found the half way point. Note that the
+ // rest of counts is not filled out.
+ if (tcount >= half) {
+ break;
+ }
+ }
+ tcount = 0;
+ int lastAdd = -1;
+
+ // These indicate what the top value for the low cube and
+ // the low value of the high cube should be in the split channel
+ // (they may not be one off if there are 'dead' spots in the
+ // counts array.)
+ int splitLo = min[splitChannel], splitHi = max[splitChannel];
+
+ for (int i = min[splitChannel]; i <= max[splitChannel]; i++) {
+ int c = counts[i];
+
+ if (c == 0) {
+
+ // No counts below this so move up bottom of cube.
+ if ((tcount == 0) && (i < max[splitChannel])) {
+ this.min[splitChannel] = i + 1;
+ }
+ continue;
+ }
+ if (tcount + c < half) {
+ lastAdd = i;
+ tcount += c;
+ continue;
+ }
+ if ((half - tcount) <= ((tcount + c) - half)) {
+
+ // Then lastAdd is a better top idx for this then i.
+ if (lastAdd == -1) {
+
+ // No lower place to break.
+ if (c == this.count) {
+
+ // All pixels are at this value so make min/max
+ // reflect that.
+ this.max[splitChannel] = i;
+ return null;// no split to make.
+ }
+ else {
+
+ // There are values about this one so
+ // split above.
+ splitLo = i;
+ splitHi = i + 1;
+ break;
+ }
+ }
+ splitLo = lastAdd;
+ splitHi = i;
+ }
+ else {
+ if (i == this.max[splitChannel]) {
+ if (c == this.count) {
+
+ // would move min up but that should
+ // have happened already.
+ return null;// no split to make.
+ }
+ else {
+
+ // Would like to break between i and i+1
+ // but no i+1 so use lastAdd and i;
+ splitLo = lastAdd;
+ splitHi = i;
+ break;
+ }
+ }
+
+ // Include c in counts
+ tcount += c;
+ splitLo = i;
+ splitHi = i + 1;
+ }
+ break;
+ }
+
+ // System.out.println("Split: " + splitChannel + "@"
+ // + splitLo + "-"+splitHi +
+ // " Count: " + tcount + " of " + count +
+ // " LA: " + lastAdd);
+ // Create the new cube and update everyone's bounds & counts.
+ Cube ret = new Cube(colors, tcount);
+
+ this.count = this.count - tcount;
+ ret.min[splitChannel] = this.min[splitChannel];
+ ret.max[splitChannel] = splitLo;
+ this.min[splitChannel] = splitHi;
+ ret.min[c0] = this.min[c0];
+ ret.max[c0] = this.max[c0];
+ ret.min[c1] = this.min[c1];
+ ret.max[c1] = this.max[c1];
+ return ret;
+ }
+
+ /**
+ * Returns the average color for this cube
+ *
+ * @return the average
+ */
+ public int averageColor() {
+ if (this.count == 0) {
+ return 0;
+ }
+ float red = 0, grn = 0, blu = 0;
+ int minR = min[0], minG = min[1], minB = min[2];
+ int maxR = max[0], maxG = max[1], maxB = max[2];
+ int[] minIdx = {minR >> 4, minG >> 4, minB >> 4};
+ int[] maxIdx = {maxR >> 4, maxG >> 4, maxB >> 4};
+ int val, ired, igrn, iblu;
+ float weight;
+
+ for (int i = minIdx[0]; i <= maxIdx[0]; i++) {
+ int idx1 = i << 8;
+
+ for (int j = minIdx[1]; j <= maxIdx[1]; j++) {
+ int idx2 = idx1 | (j << 4);
+
+ for (int k = minIdx[2]; k <= maxIdx[2]; k++) {
+ int idx = idx2 | k;
+ List v = colors[idx];
+
+ if (v == null) {
+ continue;
+ }
+ Iterator itr = v.iterator();
+ Counter c;
+
+ while (itr.hasNext()) {
+ c = (Counter) itr.next();
+ val = c.val;
+ ired = (val & 0xFF0000) >> 16;
+ igrn = (val & 0x00FF00) >> 8;
+ iblu = (val & 0x0000FF);
+ if (((ired >= minR) && (ired <= maxR)) && ((igrn >= minG) && (igrn <= maxG)) && ((iblu >= minB) && (iblu <= maxB))) {
+ weight = (c.count / (float) this.count);
+ red += ((float) ired) * weight;
+ grn += ((float) igrn) * weight;
+ blu += ((float) iblu) * weight;
+ }
+ }
+ }
+ }
+ }
+
+ // System.out.println("RGB: [" + red + ", " +
+ // grn + ", " + blu + "]");
+ return (((int) (red + 0.5f)) << 16 | ((int) (grn + 0.5f)) << 8 | ((int) (blu + 0.5f)));
+ }
+ }// end Cube
+
+ /**
+ * You cannot create this
+ */
+ private IndexImage() {
+ }
+
+ /**
+ * @param pImage the image to get {@code IndexColorModel} from
+ * @param pNumberOfColors the number of colors for the {@code IndexColorModel}
+ * @param pFast {@code true} if fast
+ * @return an {@code IndexColorModel}
+ * @see #getIndexColorModel(Image,int,int)
+ *
+ * @deprecated Use {@link #getIndexColorModel(Image,int,int)} instead!
+ * This version will be removed in a later version of the API.
+ */
+ public static IndexColorModel getIndexColorModel(Image pImage, int pNumberOfColors, boolean pFast) {
+
+ return getIndexColorModel(pImage, pNumberOfColors, pFast
+ ? COLOR_SELECTION_FAST
+ : COLOR_SELECTION_QUALITY);
+ }
+
+ /**
+ * Gets an {@code IndexColorModel} from the given image. If the image has an
+ * {@code IndexColorModel}, this will be returned. Otherwise, an {@code IndexColorModel}
+ * is created, using an adaptive pallete.
+ *
+ * @param pImage the image to get {@code IndexColorModel} from
+ * @param pNumberOfColors the number of colors for the {@code IndexColorModel}
+ * @param pHints one of {@link #COLOR_SELECTION_FAST},
+ * {@link #COLOR_SELECTION_QUALITY} or
+ * {@link #COLOR_SELECTION_DEFAULT}.
+ * @return The {@code IndexColorModel} from the given image, or a newly created
+ * {@code IndexColorModel} using an adaptive palette.
+ * @throws ImageConversionException if an exception occurred during color
+ * model extraction.
+ */
+ public static IndexColorModel getIndexColorModel(Image pImage, int pNumberOfColors, int pHints) throws ImageConversionException {
+ IndexColorModel icm = null;
+ RenderedImage image = null;
+
+ if (pImage instanceof RenderedImage) {
+ image = (RenderedImage) pImage;
+ ColorModel cm = image.getColorModel();
+
+ if (cm instanceof IndexColorModel) {
+ // Test if we have right number of colors
+ if (((IndexColorModel) cm).getMapSize() <= pNumberOfColors) {
+ //System.out.println("IndexColorModel from BufferedImage");
+ icm = (IndexColorModel) cm;// Done
+ }
+ }
+
+ // Else create from buffered image, hard way, see below
+ }
+ else {
+ // Create from image using BufferedImageFactory
+ BufferedImageFactory factory = new BufferedImageFactory(pImage);
+ ColorModel cm = factory.getColorModel();
+
+ if ((cm instanceof IndexColorModel) && ((IndexColorModel) cm).getMapSize() <= pNumberOfColors) {
+ //System.out.println("IndexColorModel from Image");
+ icm = (IndexColorModel) cm;// Done
+ }
+ else {
+ // Else create from (buffered) image, hard way
+ image = factory.getBufferedImage();
+ }
+ }
+
+ // We now have at least a buffered image, create model from it
+ if (icm == null) {
+ icm = createIndexColorModel(ImageUtil.toBuffered(image), pNumberOfColors, pHints);
+
+ //System.out.println("IndexColorModel created from colors.");
+ }
+ else if (!(icm instanceof InverseColorMapIndexColorModel)) {
+ // If possible, use faster code
+ //System.out.println("Wrappimg IndexColorModel in InverseColorMapIndexColorModel");
+ icm = new InverseColorMapIndexColorModel(icm);
+ }
+ //else {
+ //System.out.println("Allredy InverseColorMapIndexColorModel");
+ //}
+ return icm;
+ }
+
+ /**
+ * Creates an {@code IndexColorModel} from the given image, using an adaptive
+ * pallete.
+ *
+ * @param pImage the image to get {@code IndexColorModel} from
+ * @param pNumberOfColors the number of colors for the {@code IndexColorModel}
+ * @param pHints use fast mode if possible (might give slightly lower
+ * quality)
+ * @return a new {@code IndexColorModel} created from the given image
+ */
+ private static IndexColorModel createIndexColorModel(BufferedImage pImage, int pNumberOfColors, int pHints) {
+ // TODO: Use ImageUtil.hasTransparentPixels(pImage, true) ||
+ // -- haraldK, 20021024, experimental, try to use one transparent pixel
+ boolean useTransparency = isTransparent(pHints);
+
+ if (useTransparency) {
+ pNumberOfColors--;
+ }
+
+ //System.out.println("Transp: " + useTransparency + " colors: " + pNumberOfColors);
+ int width = pImage.getWidth();
+ int height = pImage.getHeight();
+
+ // Using 4 bits from R, G & B.
+ List[] colors = new List[1 << 12];// [4096]
+
+ // Speedup, doesn't decrease image quality much
+ int step = 1;
+
+ if (isFast(pHints)) {
+ step += (width * height / 16384);// 128x128px
+ }
+ int sampleCount = 0;
+ int rgb;
+
+ //for (int x = 0; x < width; x++) {
+ //for (int y = 0; y < height; y++) {
+ for (int x = 0; x < width; x++) {
+ for (int y = x % step; y < height; y += step) {
+ // Count the number of color samples
+ sampleCount++;
+
+ // Get ARGB pixel from image
+ rgb = (pImage.getRGB(x, y) & 0xFFFFFF);
+
+ // Get index from high four bits of each component.
+ int index = (((rgb & 0xF00000) >>> 12) | ((rgb & 0x00F000) >>> 8) | ((rgb & 0x0000F0) >>> 4));
+
+ // Get the 'hash vector' for that key.
+ List v = colors[index];
+
+ if (v == null) {
+ // No colors in this bin yet so create vector and
+ // add color.
+ v = new ArrayList();
+ v.add(new Counter(rgb));
+ colors[index] = v;
+ }
+ else {
+ // Find our color in the bin or create a counter for it.
+ Iterator i = v.iterator();
+
+ while (true) {
+ if (i.hasNext()) {
+ // try adding our color to each counter...
+ if (((Counter) i.next()).add(rgb)) {
+ break;
+ }
+ }
+ else {
+ v.add(new Counter(rgb));
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ // All colours found, reduce to pNumberOfColors
+ int numberOfCubes = 1;
+ int fCube = 0;
+ Cube[] cubes = new Cube[pNumberOfColors];
+
+ cubes[0] = new Cube(colors, sampleCount);
+
+ //cubes[0] = new Cube(colors, width * height);
+ while (numberOfCubes < pNumberOfColors) {
+ while (cubes[fCube].isDone()) {
+ fCube++;
+ if (fCube == numberOfCubes) {
+ break;
+ }
+ }
+ if (fCube == numberOfCubes) {
+ break;
+ }
+ Cube cube = cubes[fCube];
+ Cube newCube = cube.split();
+
+ if (newCube != null) {
+ if (newCube.count > cube.count) {
+ Cube tmp = cube;
+
+ cube = newCube;
+ newCube = tmp;
+ }
+ int j = fCube;
+ int count = cube.count;
+
+ for (int i = fCube + 1; i < numberOfCubes; i++) {
+ if (cubes[i].count < count) {
+ break;
+ }
+ cubes[j++] = cubes[i];
+ }
+ cubes[j++] = cube;
+ count = newCube.count;
+ while (j < numberOfCubes) {
+ if (cubes[j].count < count) {
+ break;
+ }
+ j++;
+ }
+ for (int i = numberOfCubes; i > j; i--) {
+ cubes[i] = cubes[i - 1];
+ }
+ cubes[j/*++*/] = newCube;
+ numberOfCubes++;
+ }
+ }
+
+ // Create RGB arrays with correct number of colors
+ // If we have transparency, the last color will be the transparent one
+ byte[] r = new byte[useTransparency ? numberOfCubes + 1 : numberOfCubes];
+ byte[] g = new byte[useTransparency ? numberOfCubes + 1 : numberOfCubes];
+ byte[] b = new byte[useTransparency ? numberOfCubes + 1 : numberOfCubes];
+
+ for (int i = 0; i < numberOfCubes; i++) {
+ int val = cubes[i].averageColor();
+
+ r[i] = (byte) ((val >> 16) & 0xFF);
+ g[i] = (byte) ((val >> 8) & 0xFF);
+ b[i] = (byte) ((val) & 0xFF);
+
+ //System.out.println("Color [" + i + "]: #" +
+ // (((val>>16)<16)?"0":"") +
+ // Integer.toHexString(val));
+ }
+
+ // For some reason using less than 8 bits causes a bug in the dither
+ // - transparency added to all totally black colors?
+ int numOfBits = 8;
+
+ // -- haraldK, 20021024, as suggested by Thomas E Deweese
+ // plus adding a transparent pixel
+ IndexColorModel icm;
+ if (useTransparency) {
+ //icm = new IndexColorModel(numOfBits, r.length, r, g, b, r.length - 1);
+ icm = new InverseColorMapIndexColorModel(numOfBits, r.length, r, g, b, r.length - 1);
+ }
+ else {
+ //icm = new IndexColorModel(numOfBits, r.length, r, g, b);
+ icm = new InverseColorMapIndexColorModel(numOfBits, r.length, r, g, b);
+ }
+ return icm;
+ }
+
+ /**
+ * Converts the input image (must be {@code TYPE_INT_RGB} or
+ * {@code TYPE_INT_ARGB}) to an indexed image. Generating an adaptive
+ * pallete (8 bit) from the color data in the image, and uses default
+ * dither.
+ *
+ * The image returned is a new image, the input image is not modified.
+ *
+ * @param pImage the BufferedImage to index and get color information from.
+ * @return the indexed BufferedImage. The image will be of type
+ * {@code BufferedImage.TYPE_BYTE_INDEXED}, and use an
+ * {@code IndexColorModel}.
+ * @see BufferedImage#TYPE_BYTE_INDEXED
+ * @see IndexColorModel
+ */
+ public static BufferedImage getIndexedImage(BufferedImage pImage) {
+ return getIndexedImage(pImage, 256, DITHER_DEFAULT);
+ }
+
+ /**
+ * Tests if the hint {@code COLOR_SELECTION_QUALITY} is not
+ * set.
+ *
+ * @param pHints hints
+ * @return true if the hint {@code COLOR_SELECTION_QUALITY}
+ * is not set.
+ */
+ private static boolean isFast(int pHints) {
+ return (pHints & COLOR_SELECTION_MASK) != COLOR_SELECTION_QUALITY;
+ }
+
+ /**
+ * Tests if the hint {@code TRANSPARENCY_BITMASK} or
+ * {@code TRANSPARENCY_TRANSLUCENT} is set.
+ *
+ * @param pHints hints
+ * @return true if the hint {@code TRANSPARENCY_BITMASK} or
+ * {@code TRANSPARENCY_TRANSLUCENT} is set.
+ */
+ static boolean isTransparent(int pHints) {
+ return (pHints & TRANSPARENCY_BITMASK) != 0 || (pHints & TRANSPARENCY_TRANSLUCENT) != 0;
+ }
+
+ /**
+ * Converts the input image (must be {@code TYPE_INT_RGB} or
+ * {@code TYPE_INT_ARGB}) to an indexed image. If the palette image
+ * uses an {@code IndexColorModel}, this will be used. Otherwise, generating an
+ * adaptive pallete (8 bit) from the given palette image.
+ * Dithering, transparency and color selection is controlled with the
+ * {@code pHints}parameter.
+ *
+ * The image returned is a new image, the input image is not modified.
+ *
+ * @param pImage the BufferedImage to index
+ * @param pPalette the Image to read color information from
+ * @param pMatte the background color, used where the original image was
+ * transparent
+ * @param pHints mHints that control output quality and speed.
+ * @return the indexed BufferedImage. The image will be of type
+ * {@code BufferedImage.TYPE_BYTE_INDEXED} or
+ * {@code BufferedImage.TYPE_BYTE_BINARY}, and use an
+ * {@code IndexColorModel}.
+ * @throws ImageConversionException if an exception occurred during color
+ * model extraction.
+ * @see #DITHER_DIFFUSION
+ * @see #DITHER_NONE
+ * @see #COLOR_SELECTION_FAST
+ * @see #COLOR_SELECTION_QUALITY
+ * @see #TRANSPARENCY_OPAQUE
+ * @see #TRANSPARENCY_BITMASK
+ * @see BufferedImage#TYPE_BYTE_INDEXED
+ * @see BufferedImage#TYPE_BYTE_BINARY
+ * @see IndexColorModel
+ */
+ public static BufferedImage getIndexedImage(BufferedImage pImage, Image pPalette, Color pMatte, int pHints)
+ throws ImageConversionException {
+ return getIndexedImage(pImage, getIndexColorModel(pPalette, 256, pHints), pMatte, pHints);
+ }
+
+ /**
+ * Converts the input image (must be {@code TYPE_INT_RGB} or
+ * {@code TYPE_INT_ARGB}) to an indexed image. Generating an adaptive
+ * pallete with the given number of colors.
+ * Dithering, transparency and color selection is controlled with the
+ * {@code pHints}parameter.
+ *
+ * The image returned is a new image, the input image is not modified.
+ *
+ * @param pImage the BufferedImage to index
+ * @param pNumberOfColors the number of colors for the image
+ * @param pMatte the background color, used where the original image was
+ * transparent
+ * @param pHints mHints that control output quality and speed.
+ * @return the indexed BufferedImage. The image will be of type
+ * {@code BufferedImage.TYPE_BYTE_INDEXED} or
+ * {@code BufferedImage.TYPE_BYTE_BINARY}, and use an
+ * {@code IndexColorModel}.
+ * @see #DITHER_DIFFUSION
+ * @see #DITHER_NONE
+ * @see #COLOR_SELECTION_FAST
+ * @see #COLOR_SELECTION_QUALITY
+ * @see #TRANSPARENCY_OPAQUE
+ * @see #TRANSPARENCY_BITMASK
+ * @see BufferedImage#TYPE_BYTE_INDEXED
+ * @see BufferedImage#TYPE_BYTE_BINARY
+ * @see IndexColorModel
+ */
+ public static BufferedImage getIndexedImage(BufferedImage pImage, int pNumberOfColors, Color pMatte, int pHints) {
+ // NOTE: We need to apply matte before creating colormodel, otherwise we
+ // won't have colors for potential faded transitions
+ IndexColorModel icm;
+
+ if (pMatte != null) {
+ icm = getIndexColorModel(createSolid(pImage, pMatte), pNumberOfColors, pHints);
+ }
+ else {
+ icm = getIndexColorModel(pImage, pNumberOfColors, pHints);
+ }
+
+ // If we found less colors, then no need to dither
+ if ((pHints & DITHER_MASK) != DITHER_NONE && (icm.getMapSize() < pNumberOfColors)) {
+ pHints = (pHints & ~DITHER_MASK) | DITHER_NONE;
+ }
+ return getIndexedImage(pImage, icm, pMatte, pHints);
+ }
+
+ /**
+ * Converts the input image (must be {@code TYPE_INT_RGB} or
+ * {@code TYPE_INT_ARGB}) to an indexed image. Using the supplied
+ * {@code IndexColorModel}'s pallete.
+ * Dithering, transparency and color selection is controlled with the
+ * {@code pHints} parameter.
+ *
+ * The image returned is a new image, the input image is not modified.
+ *
+ * @param pImage the BufferedImage to index
+ * @param pColors an {@code IndexColorModel} containing the color information
+ * @param pMatte the background color, used where the original image was
+ * transparent. Also note that any transparent antialias will be
+ * rendered against this color.
+ * @param pHints RenderingHints that control output quality and speed.
+ * @return the indexed BufferedImage. The image will be of type
+ * {@code BufferedImage.TYPE_BYTE_INDEXED} or
+ * {@code BufferedImage.TYPE_BYTE_BINARY}, and use an
+ * {@code IndexColorModel}.
+ * @see #DITHER_DIFFUSION
+ * @see #DITHER_NONE
+ * @see #COLOR_SELECTION_FAST
+ * @see #COLOR_SELECTION_QUALITY
+ * @see #TRANSPARENCY_OPAQUE
+ * @see #TRANSPARENCY_BITMASK
+ * @see BufferedImage#TYPE_BYTE_INDEXED
+ * @see BufferedImage#TYPE_BYTE_BINARY
+ * @see IndexColorModel
+ */
+ public static BufferedImage getIndexedImage(BufferedImage pImage, IndexColorModel pColors, Color pMatte, int pHints) {
+ // TODO: Consider:
+ /*
+ if (pImage.getType() == BufferedImage.TYPE_BYTE_INDEXED
+ || pImage.getType() == BufferedImage.TYPE_BYTE_BINARY) {
+ pImage = ImageUtil.toBufferedImage(pImage, BufferedImage.TYPE_INT_ARGB);
+ }
+ */
+
+ // Get dimensions
+ final int width = pImage.getWidth();
+ final int height = pImage.getHeight();
+
+ // Support transparancy?
+ boolean transparency = isTransparent(pHints) && (pImage.getColorModel().getTransparency() != Transparency.OPAQUE) && (pColors.getTransparency() != Transparency.OPAQUE);
+
+ // Create image with solid background
+ BufferedImage solid = pImage;
+
+ if (pMatte != null) {// transparency doesn't really matter
+ solid = createSolid(pImage, pMatte);
+ }
+ BufferedImage indexed;
+
+ // Support TYPE_BYTE_BINARY, but only for 2 bit images, as the default
+ // dither does not work with TYPE_BYTE_BINARY it seems...
+ if (pColors.getMapSize() > 2) {
+ indexed = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED, pColors);
+ }
+ else {
+ indexed = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_BINARY, pColors);
+ }
+
+ // Apply dither if requested
+ switch (pHints & DITHER_MASK) {
+ case DITHER_DIFFUSION:
+ case DITHER_DIFFUSION_ALTSCANS:
+ // Create a DiffusionDither to apply dither to indexed
+ DiffusionDither dither = new DiffusionDither(pColors);
+
+ if ((pHints & DITHER_MASK) == DITHER_DIFFUSION_ALTSCANS) {
+ dither.setAlternateScans(true);
+ }
+
+ dither.filter(solid, indexed);
+
+ break;
+ case DITHER_NONE:
+ // Just copy pixels, without dither
+ // NOTE: This seems to be slower than the method below, using
+ // Graphics2D.drawImage, and VALUE_DITHER_DISABLE,
+ // however you possibly end up getting a dithered image anyway,
+ // therefore, do it slower and produce correct result. :-)
+ CopyDither copy = new CopyDither(pColors);
+ copy.filter(solid, indexed);
+
+ break;
+ case DITHER_DEFAULT:
+ // This is the default
+ default:
+ // Render image data onto indexed image, using default
+ // (probably we get dither, but it depends on the GFX engine).
+ Graphics2D g2d = indexed.createGraphics();
+ try {
+ RenderingHints hints = new RenderingHints(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE);
+
+ g2d.setRenderingHints(hints);
+ g2d.drawImage(solid, 0, 0, null);
+ }
+ finally {
+ g2d.dispose();
+ }
+ break;
+ }
+
+ // Transparency support, this approach seems lame, but it's the only
+ // solution I've found until now (that actually works).
+ // Got anything to do with isPremultiplied? Hmm...
+ if (transparency) {
+ // Re-apply the alpha-channel of the original image
+ applyAlpha(indexed, pImage);
+ }
+
+ // Return the indexed BufferedImage
+ return indexed;
+ }
+
+ /**
+ * Converts the input image (must be {@code TYPE_INT_RGB} or
+ * {@code TYPE_INT_ARGB}) to an indexed image. Generating an adaptive
+ * pallete with the given number of colors.
+ * Dithering, transparency and color selection is controlled with the
+ * {@code pHints}parameter.
+ *
+ * The image returned is a new image, the input image is not modified.
+ *
+ * @param pImage the BufferedImage to index
+ * @param pNumberOfColors the number of colors for the image
+ * @param pHints mHints that control output quality and speed.
+ * @return the indexed BufferedImage. The image will be of type
+ * {@code BufferedImage.TYPE_BYTE_INDEXED} or
+ * {@code BufferedImage.TYPE_BYTE_BINARY}, and use an
+ * {@code IndexColorModel}.
+ * @see #DITHER_DIFFUSION
+ * @see #DITHER_NONE
+ * @see #COLOR_SELECTION_FAST
+ * @see #COLOR_SELECTION_QUALITY
+ * @see #TRANSPARENCY_OPAQUE
+ * @see #TRANSPARENCY_BITMASK
+ * @see BufferedImage#TYPE_BYTE_INDEXED
+ * @see BufferedImage#TYPE_BYTE_BINARY
+ * @see IndexColorModel
+ */
+ public static BufferedImage getIndexedImage(BufferedImage pImage, int pNumberOfColors, int pHints) {
+ return getIndexedImage(pImage, pNumberOfColors, null, pHints);
+ }
+
+ /**
+ * Converts the input image (must be {@code TYPE_INT_RGB} or
+ * {@code TYPE_INT_ARGB}) to an indexed image. Using the supplied
+ * {@code IndexColorModel}'s pallete.
+ * Dithering, transparency and color selection is controlled with the
+ * {@code pHints}parameter.
+ *
+ * The image returned is a new image, the input image is not modified.
+ *
+ * @param pImage the BufferedImage to index
+ * @param pColors an {@code IndexColorModel} containing the color information
+ * @param pHints RenderingHints that control output quality and speed.
+ * @return the indexed BufferedImage. The image will be of type
+ * {@code BufferedImage.TYPE_BYTE_INDEXED} or
+ * {@code BufferedImage.TYPE_BYTE_BINARY}, and use an
+ * {@code IndexColorModel}.
+ * @see #DITHER_DIFFUSION
+ * @see #DITHER_NONE
+ * @see #COLOR_SELECTION_FAST
+ * @see #COLOR_SELECTION_QUALITY
+ * @see #TRANSPARENCY_OPAQUE
+ * @see #TRANSPARENCY_BITMASK
+ * @see BufferedImage#TYPE_BYTE_INDEXED
+ * @see BufferedImage#TYPE_BYTE_BINARY
+ * @see IndexColorModel
+ */
+ public static BufferedImage getIndexedImage(BufferedImage pImage, IndexColorModel pColors, int pHints) {
+ return getIndexedImage(pImage, pColors, null, pHints);
+ }
+
+ /**
+ * Converts the input image (must be {@code TYPE_INT_RGB} or
+ * {@code TYPE_INT_ARGB}) to an indexed image. If the palette image
+ * uses an {@code IndexColorModel}, this will be used. Otherwise, generating an
+ * adaptive pallete (8 bit) from the given palette image.
+ * Dithering, transparency and color selection is controlled with the
+ * {@code pHints}parameter.
+ *
+ * The image returned is a new image, the input image is not modified.
+ *
+ * @param pImage the BufferedImage to index
+ * @param pPalette the Image to read color information from
+ * @param pHints mHints that control output quality and speed.
+ * @return the indexed BufferedImage. The image will be of type
+ * {@code BufferedImage.TYPE_BYTE_INDEXED} or
+ * {@code BufferedImage.TYPE_BYTE_BINARY}, and use an
+ * {@code IndexColorModel}.
+ * @see #DITHER_DIFFUSION
+ * @see #DITHER_NONE
+ * @see #COLOR_SELECTION_FAST
+ * @see #COLOR_SELECTION_QUALITY
+ * @see #TRANSPARENCY_OPAQUE
+ * @see #TRANSPARENCY_BITMASK
+ * @see BufferedImage#TYPE_BYTE_INDEXED
+ * @see BufferedImage#TYPE_BYTE_BINARY
+ * @see IndexColorModel
+ */
+ public static BufferedImage getIndexedImage(BufferedImage pImage, Image pPalette, int pHints) {
+ return getIndexedImage(pImage, pPalette, null, pHints);
+ }
+
+ /**
+ * Creates a copy of the given image, with a solid background
+ *
+ * @param pOriginal the original image
+ * @param pBackground the background color
+ * @return a new {@code BufferedImage}
+ */
+ private static BufferedImage createSolid(BufferedImage pOriginal, Color pBackground) {
+ // Create a temporary image of same dimension and type
+ BufferedImage solid = new BufferedImage(pOriginal.getColorModel(), pOriginal.copyData(null), pOriginal.isAlphaPremultiplied(), null);
+ Graphics2D g = solid.createGraphics();
+
+ try {
+ // Clear in background color
+ g.setColor(pBackground);
+ g.setComposite(AlphaComposite.DstOver);// Paint "underneath"
+ g.fillRect(0, 0, pOriginal.getWidth(), pOriginal.getHeight());
+ }
+ finally {
+ g.dispose();
+ }
+
+ return solid;
+ }
+
+ /**
+ * Applies the alpha-component of the alpha image to the given image.
+ * The given image is modified in place.
+ *
+ * @param pImage the image to apply alpha to
+ * @param pAlpha the image containing the alpha
+ */
+ private static void applyAlpha(BufferedImage pImage, BufferedImage pAlpha) {
+ // Apply alpha as transparancy, using threshold of 25%
+ for (int y = 0; y < pAlpha.getHeight(); y++) {
+ for (int x = 0; x < pAlpha.getWidth(); x++) {
+
+ // Get alpha component of pixel, if less than 25% opaque
+ // (0x40 = 64 => 25% of 256), the pixel will be transparent
+ if (((pAlpha.getRGB(x, y) >> 24) & 0xFF) < 0x40) {
+ pImage.setRGB(x, y, 0x00FFFFFF);// 100% transparent
+ }
+ }
+ }
+ }
+
+ /*
+ * This class is also a command-line utility.
+ */
+ public static void main(String pArgs[]) {
+
+ // Defaults
+ int argIdx = 0;
+ int speedTest = -1;
+ boolean overWrite = false;
+ boolean monochrome = false;
+ boolean gray = false;
+ int numColors = 256;
+ String dither = null;
+ String quality = null;
+ String format = null;
+ Color background = null;
+ boolean transparency = false;
+ String paletteFileName = null;
+ boolean errArgs = false;
+
+ // Parse args
+ while ((argIdx < pArgs.length) && (pArgs[argIdx].charAt(0) == '-') && (pArgs[argIdx].length() >= 2)) {
+ if ((pArgs[argIdx].charAt(1) == 's') || pArgs[argIdx].equals("--speedtest")) {
+ argIdx++;
+
+ // Get number of iterations
+ if ((pArgs.length > argIdx) && (pArgs[argIdx].charAt(0) != '-')) {
+ try {
+ speedTest = Integer.parseInt(pArgs[argIdx++]);
+ }
+ catch (NumberFormatException nfe) {
+ errArgs = true;
+ break;
+ }
+ }
+ else {
+
+ // Default to 10 iterations
+ speedTest = 10;
+ }
+ }
+ else
+ if ((pArgs[argIdx].charAt(1) == 'w') || pArgs[argIdx].equals("--overwrite")) {
+ overWrite = true;
+ argIdx++;
+ }
+ else
+ if ((pArgs[argIdx].charAt(1) == 'c') || pArgs[argIdx].equals("--colors")) {
+ argIdx++;
+ try {
+ numColors = Integer.parseInt(pArgs[argIdx++]);
+ }
+ catch (NumberFormatException nfe) {
+ errArgs = true;
+ break;
+ }
+ }
+ else
+ if ((pArgs[argIdx].charAt(1) == 'g') || pArgs[argIdx].equals("--grayscale")) {
+ argIdx++;
+ gray = true;
+ }
+ else
+ if ((pArgs[argIdx].charAt(1) == 'm') || pArgs[argIdx].equals("--monochrome")) {
+ argIdx++;
+ numColors = 2;
+ monochrome = true;
+ }
+ else
+ if ((pArgs[argIdx].charAt(1) == 'd') || pArgs[argIdx].equals("--dither")) {
+ argIdx++;
+ dither = pArgs[argIdx++];
+ }
+ else
+ if ((pArgs[argIdx].charAt(1) == 'p') || pArgs[argIdx].equals("--palette")) {
+ argIdx++;
+ paletteFileName = pArgs[argIdx++];
+ }
+ else
+ if ((pArgs[argIdx].charAt(1) == 'q') || pArgs[argIdx].equals("--quality")) {
+ argIdx++;
+ quality = pArgs[argIdx++];
+ }
+ else
+ if ((pArgs[argIdx].charAt(1) == 'b') || pArgs[argIdx].equals("--bgcolor")) {
+ argIdx++;
+ try {
+ background = StringUtil.toColor(pArgs[argIdx++]);
+ }
+ catch (Exception e) {
+ errArgs = true;
+ break;
+ }
+ }
+ else
+ if ((pArgs[argIdx].charAt(1) == 't') || pArgs[argIdx].equals("--transparency")) {
+ argIdx++;
+ transparency = true;
+ }
+ else
+ if ((pArgs[argIdx].charAt(1) == 'f') || pArgs[argIdx].equals("--outputformat")) {
+ argIdx++;
+ format = StringUtil.toLowerCase(pArgs[argIdx++]);
+ }
+ else
+ if ((pArgs[argIdx].charAt(1) == 'h') || pArgs[argIdx].equals("--help")) {
+ argIdx++;
+
+ // Setting errArgs to true, to print usage
+ errArgs = true;
+ }
+ else {
+ System.err.println("Unknown option \"" + pArgs[argIdx++] + "\"");
+ }
+ }
+ if (errArgs || (pArgs.length < (argIdx + 1))) {
+ System.err.println("Usage: IndexImage [--help|-h] [--speedtest|-s ] [--bgcolor|-b ] [--colors|-c | --grayscale|g | --monochrome|-m | --palette|-p ] [--dither|-d (default|diffusion|none)] [--quality|-q (default|high|low)] [--transparency|-t] [--outputformat|-f (gif|jpeg|png|wbmp|...)] [--overwrite|-w] [