/*
* ImageI/O-Ext - OpenSource Java Image translation Library
* http://www.geo-solutions.it/
* http://java.net/projects/imageio-ext/
* (C) 2007 - 2009, GeoSolutions
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* either version 3 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package it.geosolutions.imageio.plugins.jpeg;
import it.geosolutions.imageio.imageioimpl.EnhancedImageReadParam;
import it.geosolutions.imageio.stream.input.FileImageInputStreamExt;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.Transparency;
import java.awt.color.ColorSpace;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.ComponentColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.PixelInterleavedSampleModel;
import java.awt.image.Raster;
import java.awt.image.RenderedImage;
import java.awt.image.SampleModel;
import java.awt.image.WritableRaster;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadata;
import javax.media.jai.ImageLayout;
import magick.ImageInfo;
import magick.MagickException;
import magick.MagickImage;
/**
* {@link JpegJMagickImageReader} is a {@link GDALImageReader} able to create
* {@link RenderedImage} from JPEG files.
*
* @author Daniele Romagnoli, GeoSolutions.
* @author Simone Giannecchini, GeoSolutions.
*/
public class JpegJMagickImageReader extends ImageReader {
protected void finalize() throws Throwable {
dispose();
}
public ImageReadParam getDefaultReadParam() {
return new JpegJMagickImageReaderReadParam();
}
/**
* Implementation of {@link ImageReadParam} for this
* {@link JpegJMagickImageReader}. Actually we are using
* {@link CloneableImageReadParam} since ImageMagick guys claim that their
* library is thread safe but the moment I am getting nasty errors if I try
* to use multithreading hence I am locking the reader up.
*
* @author Simone Giannecchini, GeoSolutions.
* @author Daniele Romagnoli, GeoSolutions.
*/
public static class JpegJMagickImageReaderReadParam extends EnhancedImageReadParam {
public String toString() {
final StringBuilder buff = new StringBuilder();
buff.append("JpegJMagickImageReaderReadParam={").append("\n");
if (this.getSourceRegion() != null)
buff
.append(
"SourceRegion"
+ this.getSourceMaxProgressivePass())
.append("\n");
buff.append("sx=").append(this.getSourceXSubsampling()).append(
" sy=").append(this.getSourceYSubsampling());
buff.append("}");
return buff.toString();
}
/**
* Deep copy this instance of {@link JpegJMagickImageReaderReadParam};
*/
public Object clone() throws CloneNotSupportedException {
final JpegJMagickImageReaderReadParam retVal = new JpegJMagickImageReaderReadParam();
retVal.setInterpolationType(this.getInterpolationType());
retVal.setController(this.getController());
retVal.setDestination(getDestination());
retVal.setDestinationBands(getDestinationBands());
retVal.setDestinationOffset(getDestinationOffset());
retVal.setDestinationType(getDestinationType());
retVal.setSourceBands(getSourceBands());
retVal.setSourceProgressivePasses(getSourceMinProgressivePass(),
getSourceNumProgressivePasses());
retVal.setSourceRegion(getSourceRegion());
retVal.setSourceSubsampling(getSourceXSubsampling(),
getSourceYSubsampling(), getSubsamplingXOffset(),
getSubsamplingYOffset());
return retVal;
}
public static final int INTERPOLATION_NEAREST = 1;
public static final int INTERPOLATION_BILINEAR = 2;
private int interpolationType;
/** Constructs a default instance of <code>JP2KakaduImageReadParam</code>. */
public JpegJMagickImageReaderReadParam() {
super();
interpolationType = INTERPOLATION_NEAREST;
}
/**
* Gets <code>InterpolationType</code>.
*
* @return the interpolation algorithm which will be used when
* imageMagick need to be warped
*/
public final int getInterpolationType() {
return interpolationType;
}
/**
* Sets <code>InterpolationType</code>.
*
* @param interpolationType
* the interpolation type used during <code>WarpAffine</code>
* operation
*
* interpolationType should be one of: -<em>INTERPOLATION_NEAREST</em> -<em>INTERPOLATION_BILINEAR</em> -<em>INTERPOLATION_BICUBIC</em> -<em>INTERPOLATION_BICUBIC2</em>
*/
public final void setInterpolationType(int interpolationType) {
this.interpolationType = interpolationType;
}
/** Initilize this JpegJMagickImageReaderReadParam */
protected void intialize(ImageReadParam param) {
if (param.hasController()) {
setController(param.getController());
}
setSourceRegion(param.getSourceRegion());
setSourceBands(param.getSourceBands());
setDestinationBands(param.getDestinationBands());
setDestination(param.getDestination());
setDestinationOffset(param.getDestinationOffset());
setSourceSubsampling(param.getSourceXSubsampling(), param
.getSourceYSubsampling(), param.getSubsamplingXOffset(),
param.getSubsamplingYOffset());
setDestinationType(param.getDestinationType());
}
}
/**
* {@link MagickImageAdapter} containes code to adapt a {@link MagickImage}
* to a {@link BufferedImage}.
*
* @author Simone Giannecchini, GeoSolutions.
*
*/
public static class MagickImageAdapter {
/**
* Caches the layout for the underlying {@link #imageMagick}.
*/
private final ImageLayout layout;
/**
* {@link MagickImage} to adapt to the {@link BufferedImage} interface.
*/
private final MagickImage imageMagick;
/**
* Constructor. Let us build a {@link MagickImageAdapter} adapter for
* the specified {@link ImageInfo} object.
*
* @param info
* @throws MagickException
*/
public MagickImageAdapter(final ImageInfo info) throws MagickException {
// TODO if we invert them JVM crashes, not nice, not at all.
imageMagick = new MagickImage(info);
final Dimension dim = imageMagick.getDimension();
final int w = dim.width;
final int h = dim.height;
final ImageLayout layout = new ImageLayout();
layout.setWidth(w).setHeight(h).setSampleModel(
new PixelInterleavedSampleModel(DataBuffer.TYPE_BYTE, h, h,
3, 3 * dim.width, new int[] { 0, 1, 2 }))
.setColorModel(
new ComponentColorModel(ColorSpace
.getInstance(ColorSpace.CS_LINEAR_RGB),
false, false, Transparency.OPAQUE,
DataBuffer.TYPE_BYTE));
this.layout = layout;
}
public ImageLayout getLayout() {
return layout;
}
public void dispose() {
this.imageMagick.destroyImages();
}
public String toString() {
final StringBuilder buff = new StringBuilder();
buff.append("MagickImageAdapter={").append("\n");
if (this.layout != null)
buff.append("ImageLayout " + this.layout.toString()).append(
"\n");
buff.append("sx=").append(this.imageMagick.toString());
buff.append("}");
return buff.toString();
}
/**
* @param magickImage
* @return
* @throws MagickException
*/
public static WritableRaster JMagickToWritableRaster(
MagickImage magickImage) throws IOException {
try {
final Dimension dim = magickImage.getDimension();
final int size = dim.width * dim.height;
byte[] pixxels = new byte[size * 3];
magickImage.dispatchImage(0, 0, dim.width, dim.height, "RGB",
pixxels);
final SampleModel sm = new PixelInterleavedSampleModel(
DataBuffer.TYPE_BYTE, dim.width, dim.height, 3,
3 * dim.width, new int[] { 0, 1, 2 });
final WritableRaster raster = Raster.createWritableRaster(sm,
new DataBufferByte(pixxels, size, 0), null);
return raster;
} catch (MagickException e) {
final IOException ioe = new IOException();
ioe.initCause(e);
throw ioe;
}
}
/**
* Get a <code>BufferedImage</code> from a {@link MagickImage}
*
* @param magickImage
* The source {@link MagickImage}
* @return a <code>BufferedImage</code> from a {@link MagickImage}
*
* @throws IOException
*/
public static BufferedImage magickImageToBufferedImage(
MagickImage magickImage) throws IOException {
final WritableRaster raster = MagickImageAdapter
.JMagickToWritableRaster(magickImage);
ColorModel cm = new ComponentColorModel(ColorSpace
.getInstance(ColorSpace.CS_LINEAR_RGB), false, false,
Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
return new BufferedImage(cm, raster, false, null);
}
public static BufferedImage magickImageToBufferedImage(
MagickImageAdapter image, Rectangle srcRegion, int dstWidth,
int dstHeight) throws IOException {
try {
MagickImage im = srcRegion != null ? image.imageMagick
.cropImage(srcRegion) : image.imageMagick;
final Dimension dim = im.getDimension();
im = (dim.width != dstWidth || dim.height != dstHeight) ? im
.sampleImage(dstWidth, dstHeight).sampleImage(dstWidth,
dstHeight) : im;
return magickImageToBufferedImage(im);
} catch (MagickException e) {
final IOException ioe = new IOException();
ioe.initCause(e);
throw ioe;
}
}
}
private static final Logger LOGGER = Logger
.getLogger("it.geosolutions.imageio.plugins.jpeg");
/** The source of the image*/
private File sourceFile;
public JpegJMagickImageReader(JpegJMagickImageReaderSpi originatingProvider) {
super(originatingProvider);
if (LOGGER.isLoggable(Level.FINE))
LOGGER.fine("JpegJMagickImageReader Constructor");
}
/**
* List of the {@link ImageLayout}s of the various images contained in
* {@link #sourceFile}.
*/
private final List<MagickImageAdapter> imagesLayouts =Collections.synchronizedList(new ArrayList<MagickImageAdapter>());
/**
* Returns the height in pixels of the given image within the input source.
*
* @param imageIndex
* the index of the image to be queried.
* @return the height of the image, as an int.
*/
public int getHeight(int imageIndex) throws IOException {
synchronized (imagesLayouts) {
checkImageIndex(imageIndex);
return imagesLayouts.get(imageIndex).getLayout().getHeight(null);
}
}
/**
* Returns the width in pixels of the given image within the input source.
*
* @param imageIndex
* the index of the image to be queried.
* @return the width of the image, as an int.
*/
public int getWidth(int imageIndex) throws IOException {
synchronized (imagesLayouts) {
checkImageIndex(imageIndex);
return imagesLayouts.get(imageIndex).getLayout().getWidth(null);
}
}
public Iterator<ImageTypeSpecifier> getImageTypes(int imageIndex) throws IOException {
synchronized (imagesLayouts) {
checkImageIndex(imageIndex);
final ImageLayout layout = (imagesLayouts.get(imageIndex)).getLayout();
return Collections.singletonList(
new ImageTypeSpecifier(layout.getColorModel(null), layout.getSampleModel(null))).iterator();
}
}
public int getNumImages(boolean allowSearch) throws IOException {
// @todo we need to change this
return 1;
}
/**
* Actually, this method is not supported and it throws an
* <code>UnsupportedOperationException</code>
*/
public IIOMetadata getImageMetadata(int imageIndex) throws IOException {
throw new UnsupportedOperationException();
}
/**
* Actually, this method is not supported and it throws an
* <code>UnsupportedOperationException</code>
*/
public IIOMetadata getStreamMetadata() throws IOException {
throw new UnsupportedOperationException();
}
/**
* Read the imageMagick and returns it as a complete
* <code>BufferedImage</code>, using a supplied
* <code>ImageReadParam</code>.
*
* @param imageIndex
* the index of the image to be retrieved.
* @param param
* an <code>ImageReadParam</code> used to control the reading
* process, or null.
*/
public BufferedImage read(int imageIndex, ImageReadParam param)
throws IOException {
synchronized (imagesLayouts) {
checkImageIndex(imageIndex);
if (LOGGER.isLoggable(Level.FINE))
LOGGER.fine("Requesting imageMagick at index " + imageIndex
+ " with ImageReadParam=" + param.toString());
// ///////////////////////////////////////////////////////////
//
// STEP 1.
// -------
// local variables initialization
//
// ///////////////////////////////////////////////////////////
// width and height for this imageMagick
int width = 0;
int height = 0;
final MagickImageAdapter im = ((MagickImageAdapter) imagesLayouts.get(imageIndex));
if (LOGGER.isLoggable(Level.FINE))
LOGGER.fine("Selected imageMagick adapter " + im.toString());
final ImageLayout layout = im.getLayout();
width = layout.getWidth(null);
height = layout.getHeight(null);
// get a default set of ImageReadParam if needed.
if (param == null)
param = getDefaultReadParam();
// The destination imageMagick properties
int dstWidth = -1;
int dstHeight = -1;
// int dstXOffset = 0;
// int dstYOffset = 0;
// The source region imageMagick properties
int srcRegionWidth = -1;
int srcRegionHeight = -1;
int srcRegionXOffset = 0;
int srcRegionYOffset = 0;
// Subsampling Factors */
int xSubsamplingFactor = -1;
int ySubsamplingFactor = -1;
// ///////////////////////////////////////////////////////////
//
// STEP 2.
// -------
// parameters management (retrieve user defined readParam and
// futher initializations)
//
// ///////////////////////////////////////////////////////////
// //
// Retrieving Information about Source Region and doing additional
// intialization operations.
// //
Rectangle srcRegion = param.getSourceRegion();
if (srcRegion != null) {
srcRegionWidth = (int) srcRegion.getWidth();
srcRegionHeight = (int) srcRegion.getHeight();
srcRegionXOffset = (int) srcRegion.getX();
srcRegionYOffset = (int) srcRegion.getY();
// ////////////////////////////////////////////////////////////////
//
// Minimum correction for wrong source regions
//
// When you do subsampling or source subsetting it might happen
// that
// the given source region in the read param is uncorrect, which
// means it can be or a bit larger than the original file or can
// begin a bit before original limits.
//
// We got to be prepared to handle such case in order to avoid
// generating ArrayIndexOutOfBoundsException later in the code.
//
// ////////////////////////////////////////////////////////////////
if (srcRegionXOffset < 0)
srcRegionXOffset = 0;
if (srcRegionYOffset < 0)
srcRegionYOffset = 0;
// initializing destination imageMagick properties
dstWidth = srcRegionWidth;
if ((srcRegionXOffset + srcRegionWidth) > width) {
srcRegionWidth = width - srcRegionXOffset;
}
dstHeight = srcRegionHeight;
if ((srcRegionYOffset + srcRegionHeight) > height) {
srcRegionHeight = height - srcRegionYOffset;
}
// creating a correct source region
srcRegion = new Rectangle(srcRegionXOffset, srcRegionYOffset,
srcRegionWidth, srcRegionHeight);
} else {
// Source Region not specified.
// Assuming Source Region Dimension equal to Source Image
// Dimension
dstWidth = width;
dstHeight = height;
// dstXOffset = dstYOffset = 0;
srcRegionWidth = width;
srcRegionHeight = height;
srcRegionXOffset = srcRegionYOffset = 0;
}
// SubSampling variables initialization
xSubsamplingFactor = param.getSourceXSubsampling();
ySubsamplingFactor = param.getSourceYSubsampling();
// ////////////////////////////////////////////////////////////////////
//
// Updating the destination size in compliance with the subSampling
// parameters
//
// ////////////////////////////////////////////////////////////////////
dstWidth = ((dstWidth - 1) / xSubsamplingFactor) + 1;
dstHeight = ((dstHeight - 1) / ySubsamplingFactor) + 1;
// ////////////////////////////////////////////////////////////////
//
// STEP 3.
// -------
// BufferedImage creation
//
// ////////////////////////////////////////////////////////////////
return MagickImageAdapter.magickImageToBufferedImage(im, srcRegion,dstWidth, dstHeight);
}
}
/**
* Check if the provided imageIndex is valid
*
* @param imageIndex
* the image index to be checked
* @throws IOException
*/
private void checkImageIndex(int imageIndex) throws IOException {
if (imageIndex > 0)
throw new IndexOutOfBoundsException();
assert Thread.holdsLock(imagesLayouts);
if (imagesLayouts.size() - 1 < imageIndex) {
try {
imagesLayouts
.add(new JpegJMagickImageReader.MagickImageAdapter(
new ImageInfo(sourceFile.getAbsolutePath())));
} catch (MagickException e) {
final IOException ioe = new IOException();
ioe.initCause(e);
throw ioe;
}
}
}
/**
* Sets the input source to use to the given <code>Object</code>, usually
* a <code>File</code> or a <code>FileImageInputStreamExt</code>
*/
public void setInput(Object input, boolean seekForwardOnly,
boolean ignoreMetadata) {
setInput(input);
super.setInput(sourceFile, seekForwardOnly, ignoreMetadata);
}
/**
* Sets the input source to use to the given <code>Object</code>, usually
* a <code>File</code> or a <code>FileImageInputStreamExt</code>
*/
public void setInput(Object input, boolean seekForwardOnly) {
setInput(input);
super.setInput(sourceFile, seekForwardOnly);
}
/**
* Sets the input source to use to the given <code>Object</code>, usually
* a <code>File</code> or a <code>FileImageInputStreamExt</code>
*/
public void setInput(Object input) {
if (LOGGER.isLoggable(Level.FINE))
LOGGER.fine("setInput on object " + input.toString());
if (input instanceof File) {
sourceFile = (File) input;
} else if (input instanceof FileImageInputStreamExt) {
FileImageInputStreamExt imageIn = (FileImageInputStreamExt) input;
sourceFile = imageIn.getFile();
} else
throw new IllegalArgumentException("The input type provided is not supported");
}
/**
* Allows any resources held by this object to be released.
*/
public void dispose() {
if (LOGGER.isLoggable(Level.FINE))
LOGGER.fine("Disposing JpegJMagickImageReader");
synchronized (imagesLayouts) {
final Iterator<MagickImageAdapter> it = imagesLayouts.iterator();
while (it.hasNext()) {
final MagickImageAdapter img = (MagickImageAdapter) it.next();
img.dispose();
}
}
super.dispose();
}
}