/*
* 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 com.twelvemonkeys.lang.Validate;
import java.awt.*;
import java.awt.image.*;
import java.lang.reflect.Array;
import java.util.EventListener;
import java.util.Hashtable;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* 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.
* <p/>
* 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.
* <p/>
* NOTE: Does not support images with more than one {@code ColorModel} or
* different types of pixel data. This is not very common.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @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<ProgressListener> listeners;
private int percentageDone;
private ImageProducer producer;
private ImageConversionException consumerException;
private volatile boolean fetching;
private boolean readColorModelOnly;
private int x = 0;
private int y = 0;
private int width = -1;
private int height = -1;
private int xSub = 1;
private int ySub = 1;
private int offset;
private int scanSize;
private ColorModel sourceColorModel;
private Hashtable sourceProperties; // ImageConsumer API dictates Hashtable
private Object sourcePixels;
private BufferedImage buffered;
private ColorModel colorModel;
// NOTE: Just to not expose the inheritance
private final Consumer consumer = new Consumer();
/**
* Creates a {@code BufferedImageFactory}.
* @param pSource the source image
* @throws IllegalArgumentException if {@code pSource == null}
*/
public BufferedImageFactory(final Image pSource) {
this(pSource != null ? pSource.getSource() : null);
}
/**
* Creates a {@code BufferedImageFactory}.
* @param pSource the source image producer
* @throws IllegalArgumentException if {@code pSource == null}
*/
public BufferedImageFactory(final ImageProducer pSource) {
Validate.notNull(pSource, "source");
producer = 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 buffered;
}
/**
* 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 buffered != null ? buffered.getColorModel() : colorModel;
}
/**
* Frees resources used by this {@code BufferedImageFactory}.
*/
public void dispose() {
freeResources();
buffered = null;
colorModel = null;
}
/**
* Aborts the image production.
*/
public void abort() {
consumer.imageComplete(ImageConsumer.IMAGEABORTED);
}
/**
* Sets the source region (AOI) for the new image.
*
* @param pRegion the source region
*/
public void setSourceRegion(final Rectangle pRegion) {
// Re-fetch everything, if region changed
if (x != pRegion.x || y != pRegion.y || width != pRegion.width || height != pRegion.height) {
dispose();
}
x = pRegion.x;
y = pRegion.y;
width = pRegion.width;
height = pRegion.height;
}
/**
* Sets the source subsampling for the new image.
*
* @param pXSub horizontal subsampling factor
* @param pYSub vertical subsampling factor
*/
public void setSourceSubsampling(int pXSub, int pYSub) {
// Re-fetch everything, if subsampling changed
if (xSub != pXSub || ySub != pYSub) {
dispose();
}
if (pXSub > 1) {
xSub = pXSub;
}
if (pYSub > 1) {
ySub = pYSub;
}
}
private synchronized void doFetch(boolean pColorModelOnly) throws ImageConversionException {
if (!fetching && (!pColorModelOnly && buffered == null || buffered == null && sourceColorModel == null)) {
// NOTE: Subsampling is only applied if extracting full image
if (!pColorModelOnly && (xSub > 1 || ySub > 1)) {
// If only sampling a region, the region must be scaled too
if (width > 0 && height > 0) {
width = (width + xSub - 1) / xSub;
height = (height + ySub - 1) / ySub;
x = (x + xSub - 1) / xSub;
y = (y + ySub - 1) / ySub;
}
producer = new FilteredImageSource(producer, new SubsamplingFilter(xSub, ySub));
}
// Start fetching
fetching = true;
readColorModelOnly = pColorModelOnly;
producer.startProduction(consumer); // Note: If single-thread (synchronous), this call will block
// Wait until the producer wakes us up, by calling imageComplete
while (fetching) {
try {
wait(200l);
}
catch (InterruptedException e) {
throw new ImageConversionException("Image conversion aborted: " + e.getMessage(), e);
}
}
if (consumerException != null) {
throw new ImageConversionException("Image conversion failed: " + consumerException.getMessage(), consumerException);
}
if (pColorModelOnly) {
createColorModel();
}
else {
createBuffered();
}
}
}
private void createColorModel() {
colorModel = sourceColorModel;
// Clean up, in case any objects are copied/cloned, so we can free resources
freeResources();
}
private void createBuffered() {
if (width > 0 && height > 0) {
if (sourceColorModel != null && sourcePixels != null) {
// TODO: Fix pixel size / color model problem
WritableRaster raster = ImageUtil.createRaster(width, height, sourcePixels, sourceColorModel);
buffered = new BufferedImage(sourceColorModel, raster, sourceColorModel.isAlphaPremultiplied(), sourceProperties);
}
else {
buffered = ImageUtil.createClear(width, height, null);
}
}
// Clean up, in case any objects are copied/cloned, so we can free resources
freeResources();
}
private void freeResources() {
sourceColorModel = null;
sourcePixels = null;
sourceProperties = null;
}
private void processProgress(int scanline) {
if (listeners != null) {
int percent = 100 * scanline / height;
if (percent > percentageDone) {
percentageDone = percent;
for (ProgressListener listener : listeners) {
listener.progress(this, percent);
}
}
}
}
/**
* Adds a progress listener to this factory.
*
* @param pListener the progress listener
*/
public void addProgressListener(ProgressListener pListener) {
if (pListener == null) {
return;
}
if (listeners == null) {
listeners = new CopyOnWriteArrayList<ProgressListener>();
}
listeners.add(pListener);
}
/**
* Removes a progress listener from this factory.
*
* @param pListener the progress listener
*/
public void removeProgressListener(ProgressListener pListener) {
if (pListener == null) {
return;
}
if (listeners == null) {
return;
}
listeners.remove(pListener);
}
/**
* Removes all progress listeners from this factory.
*/
public void removeAllProgressListeners() {
if (listeners != null) {
listeners.clear();
}
}
/**
* Converts an array of {@code int} pixels to an array of {@code short}
* pixels. The conversion is done, by masking out the
* <em>higher 16 bits</em> of the {@code int}.
*
* For any given {@code int}, the {@code short} value is computed as
* follows:
* <blockquote>{@code
* short value = (short) (intValue & 0x0000ffff);
* }</blockquote>
*
* @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 percentage 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
*/
@SuppressWarnings({"SuspiciousSystemArraycopy"})
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;
}
// Allocate array if necessary
if (sourcePixels == null) {
// 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?
sourcePixels = Array.newInstance(pPixels.getClass().getComponentType(), width * height);
scanSize = width;
offset = 0;
}
else if (sourcePixels.getClass() != pPixels.getClass()) {
throw new IllegalStateException("Only one pixel type allowed");
}
// AOI stuff
if (pY < y) {
int diff = y - pY;
if (diff >= pHeight) {
return;
}
pOffset += pScanSize * diff;
pY += diff;
pHeight -= diff;
}
if (pY + pHeight > y + height) {
pHeight = (y + height) - pY;
if (pHeight <= 0) {
return;
}
}
if (pX < x) {
int diff = x - pX;
if (diff >= pWidth) {
return;
}
pOffset += diff;
pX += diff;
pWidth -= diff;
}
if (pX + pWidth > x + width) {
pWidth = (x + width) - pX;
if (pWidth <= 0) {
return;
}
}
int dstOffset = offset + (pY - y) * scanSize + (pX - x);
// Do the pixel copying
for (int i = pHeight; i > 0; i--) {
System.arraycopy(pPixels, pOffset, sourcePixels, dstOffset, pWidth);
pOffset += pScanSize;
dstOffset += scanSize;
}
processProgress(pY + pHeight);
}
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(final ColorModel pModel) {
// NOTE: There seems to be a "bug" in AreaAveragingScaleFilter, as it
// first passes the original color model 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 (sourceColorModel != pModel) {
if (/*sourceColorModel == null ||*/ sourcePixels == null) {
sourceColorModel = pModel;
}
else {
throw new IllegalStateException("Change of ColorModel after pixel delivery not supported");
}
}
// If color model is all we ask for, stop now
if (readColorModelOnly) {
consumer.imageComplete(ImageConsumer.IMAGEABORTED);
}
}
public void imageComplete(int pStatus) {
fetching = false;
if (producer != null) {
producer.removeConsumer(this);
}
switch (pStatus) {
case ImageConsumer.IMAGEERROR:
consumerException = new ImageConversionException("ImageConsumer.IMAGEERROR");
break;
}
synchronized (BufferedImageFactory.this) {
BufferedImageFactory.this.notifyAll();
}
}
public void setColorModel(ColorModel pModel) {
setColorModelOnce(pModel);
}
public void setDimensions(int pWidth, int pHeight) {
if (width < 0) {
width = pWidth - x;
}
if (height < 0) {
height = pHeight - y;
}
// Hmm.. Special case, but is it a good idea?
if (width <= 0 || height <= 0) {
imageComplete(ImageConsumer.STATICIMAGEDONE);
}
}
public void setHints(int pHintflags) {
// ignore
}
public void setPixels(int pX, int pY, int pWidth, int pHeight, ColorModel pModel, byte[] pPixels, int pOffset, int pScanSize) {
setPixelsImpl(pX, pY, pWidth, pHeight, pModel, pPixels, pOffset, pScanSize);
}
public void setPixels(int pX, int pY, int pWeigth, int pHeight, ColorModel pModel, int[] pPixels, int pOffset, int pScanSize) {
if (pModel.getTransferType() == 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);
}
}
public void setProperties(Hashtable pProperties) {
sourceProperties = pProperties;
}
}
/*
public static void main(String[] args) throws InterruptedException {
Image image = Toolkit.getDefaultToolkit().createImage(args[0]);
System.err.printf("image: %s (which is %sa buffered image)\n", image, image instanceof BufferedImage ? "" : "not ");
int warmUpLoops = 500;
int testLoops = 100;
for (int i = 0; i < warmUpLoops; i++) {
// Warm up...
convertUsingFactory(image);
convertUsingPixelGrabber(image);
convertUsingPixelGrabberNaive(image);
}
BufferedImage bufferedImage = null;
long start = System.currentTimeMillis();
for (int i = 0; i < testLoops; i++) {
bufferedImage = convertUsingFactory(image);
}
System.err.printf("Conversion time (factory): %f ms (image: %s)\n", (System.currentTimeMillis() - start) / (double) testLoops, bufferedImage);
start = System.currentTimeMillis();
for (int i = 0; i < testLoops; i++) {
bufferedImage = convertUsingPixelGrabber(image);
}
System.err.printf("Conversion time (grabber): %f ms (image: %s)\n", (System.currentTimeMillis() - start) / (double) testLoops, bufferedImage);
start = System.currentTimeMillis();
for (int i = 0; i < testLoops; i++) {
bufferedImage = convertUsingPixelGrabberNaive(image);
}
System.err.printf("Conversion time (naive g): %f ms (image: %s)\n", (System.currentTimeMillis() - start) / (double) testLoops, bufferedImage);
}
private static BufferedImage convertUsingPixelGrabberNaive(Image image) throws InterruptedException {
// NOTE: It does not matter if we wait for the image or not, the time is about the same as it will only happen once
if ((image.getWidth(null) < 0 || image.getHeight(null) < 0) && !ImageUtil.waitForImage(image)) {
System.err.printf("Could not get image dimensions for image %s\n", image.getSource());
}
int w = image.getWidth(null);
int h = image.getHeight(null);
PixelGrabber grabber = new PixelGrabber(image, 0, 0, w, h, true); // force RGB
grabber.grabPixels();
// Following casts are safe, as we force RGB in the pixel grabber
int[] pixels = (int[]) grabber.getPixels();
BufferedImage bufferedImage = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
// bufferedImage.setRGB(0, 0, w, h, pixels, 0, w);
bufferedImage.getRaster().setDataElements(0, 0, w, h, pixels);
return bufferedImage;
}
private static BufferedImage convertUsingPixelGrabber(Image image) throws InterruptedException {
// NOTE: It does not matter if we wait for the image or not, the time is about the same as it will only happen once
if ((image.getWidth(null) < 0 || image.getHeight(null) < 0) && !ImageUtil.waitForImage(image)) {
System.err.printf("Could not get image dimensions for image %s\n", image.getSource());
}
int w = image.getWidth(null);
int h = image.getHeight(null);
PixelGrabber grabber = new PixelGrabber(image, 0, 0, w, h, true); // force RGB
grabber.grabPixels();
// Following casts are safe, as we force RGB in the pixel grabber
// DirectColorModel cm = (DirectColorModel) grabber.getColorModel();
DirectColorModel cm = (DirectColorModel) ColorModel.getRGBdefault();
int[] pixels = (int[]) grabber.getPixels();
WritableRaster raster = Raster.createPackedRaster(new DataBufferInt(pixels, pixels.length), w, h, w, cm.getMasks(), null);
return new BufferedImage(cm, raster, cm.isAlphaPremultiplied(), null);
}
private static BufferedImage convertUsingFactory(Image image) {
return new BufferedImageFactory(image).getBufferedImage();
}
*/
}