com.twelvemonkeys.image.BufferedImageFactory Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of org.apache.fop Show documentation
Show all versions of org.apache.fop Show documentation
The core maven build properties
The 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 - 2025 Weber Informatics LLC | Privacy Policy