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

com.twelvemonkeys.image.BufferedImageFactory Maven / Gradle / Ivy

There is a newer version: 1.2.2.1-jre17
Show newest version
/*
 * 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 of the copyright holder 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 HOLDER 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.
 * 

* 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 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(); } 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 * higher 16 bits of the {@code int}. * * For any 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 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(); } */ }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy