/* * Geotoolkit.org - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2003-2012, Open Source Geospatial Foundation (OSGeo) * (C) 2009-2012, Geomatys * * 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; * version 2.1 of the License. * * 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 org.geotoolkit.image.io.plugin; import java.awt.Dimension; import java.awt.Rectangle; import java.awt.image.BufferedImage; import java.awt.image.DataBuffer; import java.awt.image.DataBufferByte; import java.awt.image.DataBufferDouble; import java.awt.image.DataBufferFloat; import java.awt.image.DataBufferInt; import java.awt.image.DataBufferShort; import java.awt.image.DataBufferUShort; import java.awt.image.SampleModel; import java.awt.image.WritableRaster; import java.io.EOFException; import java.io.IOException; import java.util.Iterator; import java.util.Locale; import javax.imageio.IIOException; import javax.imageio.ImageReadParam; import javax.imageio.ImageReader; import javax.imageio.ImageTypeSpecifier; import javax.imageio.spi.ImageReaderSpi; import javax.imageio.spi.ServiceRegistry; import javax.imageio.stream.ImageInputStream; import javax.media.jai.iterator.RectIterFactory; import javax.media.jai.iterator.WritableRectIter; import com.sun.media.imageio.stream.RawImageInputStream; import org.geotoolkit.image.SampleModels; import org.geotoolkit.image.io.SampleConverter; import org.geotoolkit.image.io.SpatialImageReader; import org.geotoolkit.lang.SystemOverride; import org.geotoolkit.resources.Errors; import org.geotoolkit.resources.Descriptions; import org.apache.sis.util.ArraysExt; import org.apache.sis.util.Classes; import org.apache.sis.util.logging.Logging; import org.geotoolkit.image.io.UnsupportedImageFormatException; /** * Image reader for raw binary files. The default implementation can process only inputs of type * {@link RawImageInputStream}. However subclasses can process arbitrary {@link ImageInputStream} * if they override the following methods: * <p> * <ul> * <li>{@link #getWidth(int)} (mandatory)</li> * <li>{@link #getHeight(int)} (mandatory)</li> * <li>{@link #getRawDataType(int)} (mandatory unless the default value, * which is {@link DataBuffer#TYPE_FLOAT}, is suitable)</li> * </ul> * <p> * This class provides similar functionalities than the RAW image reader provided by the * <cite>Image I/O extensions for JAI</cite> library. The main difference is that this class * can be extended, and provides support for color palette and "<cite>no data value</cite>" * conversion as documented in the super-class. Experience also suggests that this class is * faster at least for floating point values. In addition, version 1.1 of the <cite>Image I/O * extension for JAI</cite> seems to have a bug in their reading of subsampled floating point * values. * * {@section Restrictions on the sample model} * The current implementations requires that the image in the stream has a * {@linkplain SampleModels#getPixelStride(SampleModel) pixel stride} equals to 1. * If the pixel stride may be different, consider using the reader provided by the * <cite>Image I/O extensions for JAI</cite> library instead. * * @author Martin Desruisseaux (Geomatys) * @version 3.07 * * @see RawImageInputStream * * @since 3.07 (derived from 2.0) * @module */ public class RawImageReader extends SpatialImageReader { /** * Constructs a new image reader. * * @param provider the {@link ImageReaderSpi} that is invoking this constructor, or null. */ public RawImageReader(final Spi provider) { super(provider); } /** * Returns the number of images available from the current input source. The default * implementation fetches this information from the input stream if it is of kind * {@link RawImageInputStream}, or delegates to the * {@linkplain SpatialImageReader#getNumImages(boolean) super-class} otherwise. * * @throws IllegalStateException if the input source has not been set. * @throws IOException if an error occurs reading the information from the input source. */ @Override public int getNumImages(final boolean allowSearch) throws IllegalStateException, IOException { if (input instanceof RawImageInputStream) { return ((RawImageInputStream) input).getNumImages(); } return super.getNumImages(allowSearch); } /** * Returns the image's width. The default implementation fetches this information from the * input stream if it is of kind {@link RawImageInputStream}, or thrown an exception otherwise. * Subclasses can override this method if they can get the image width in an other way. * * @throws IOException If the width can not be obtained of an I/O error occurred. */ @Override public int getWidth(final int imageIndex) throws IOException { checkImageIndex(imageIndex); if (input instanceof RawImageInputStream) { final Dimension size = ((RawImageInputStream) input).getImageDimension(imageIndex); if (size != null) { return size.width; } } throw new IIOException(Errors.format(Errors.Keys.UnspecifiedImageSize)); } /** * Returns the image's height. The default implementation fetches this information from the * input stream if it is of kind {@link RawImageInputStream}, or thrown an exception otherwise. * Subclasses can override this method if they can get the height width in an other way. * * @throws IOException If the height can not be obtained of an I/O error occurred. */ @Override public int getHeight(final int imageIndex) throws IOException { checkImageIndex(imageIndex); if (input instanceof RawImageInputStream) { final Dimension size = ((RawImageInputStream) input).getImageDimension(imageIndex); if (size != null) { return size.height; } } throw new IIOException(Errors.format(Errors.Keys.UnspecifiedImageSize)); } /** * Returns the number of bands available for the specified image. The default * implementation fetches this information from the input stream if it is of kind * {@link RawImageInputStream}, or delegates to the * {@linkplain SpatialImageReader#getNumBands(int) super-class} otherwise. * * @throws IOException if an error occurs reading the information from the input source. */ @Override public int getNumBands(final int imageIndex) throws IOException { if (input instanceof RawImageInputStream) { final ImageTypeSpecifier type = ((RawImageInputStream) input).getImageType(); if (type != null) { return type.getSampleModel().getNumBands(); } } return super.getNumBands(imageIndex); } /** * Returns the data type which most closely represents the "raw" internal data of the image. * It should be one of {@link DataBuffer} constants. The default implementation fetches this * information from the input stream if it is of kind {@link RawImageInputStream}, or returns * {@link DataBuffer#TYPE_FLOAT} otherwise. * <p> * Subclasses can override this method if they can get the data type in an other way. * See the {@linkplain SpatialImageReader#getRawDataType(int) super-class javadoc} * for an explanation about how the returned type is used. * * @param imageIndex The index of the image to be queried. * @return The data type. * @throws IOException If an error occurs reading the format information from the input source. */ @Override protected int getRawDataType(final int imageIndex) throws IOException { checkImageIndex(imageIndex); if (input instanceof RawImageInputStream) { final ImageTypeSpecifier type = ((RawImageInputStream) input).getImageType(); if (type != null) { return type.getSampleModel().getDataType(); } } return super.getRawDataType(imageIndex); } /** * Returns the data type which most closely represents the "raw" internal data of the image. * The default implementation fetches this information from the input stream if it is of kind * {@link RawImageInputStream}, or delegates to the * {@linkplain SpatialImageReader#getRawImageType(int) super-class} otherwise. */ @Override public ImageTypeSpecifier getRawImageType(final int imageIndex) throws IOException { checkImageIndex(imageIndex); if (input instanceof RawImageInputStream) { final ImageTypeSpecifier type = ((RawImageInputStream) input).getImageType(); if (type != null) { return type; } } return super.getRawImageType(imageIndex); } /** * Returns {@code true} since random access is easy in uncompressed images. */ @Override public boolean isRandomAccessEasy(final int imageIndex) throws IOException { checkImageIndex(imageIndex); return true; } /** * Reads the image indexed by {@code imageIndex}. * * @param imageIndex The index of the image to be retrieved. * @param param Parameters used to control the reading process, or null. * @return The desired portion of the image. * @throws IOException if an input operation failed. */ @Override public BufferedImage read(final int imageIndex, final ImageReadParam param) throws IOException { checkImageIndex(imageIndex); clearAbortRequest(); final ImageInputStream input = (ImageInputStream) getInput(); final ImageTypeSpecifier imageType = getRawImageType(imageIndex); final SampleModel streamModel = imageType.getSampleModel(); if (SampleModels.getPixelStride(streamModel) != 1) { throw new UnsupportedImageFormatException(Errors.format(Errors.Keys.UnsupportedFileType_1, Classes.getShortClassName(streamModel) + "[pixelStride = " + SampleModels.getPixelStride(streamModel) + ']')); } /* * Get informations about the layout of the image in the stream. * If the user requested some image other than the one at index 0, * advance to that image. */ final int width = getWidth (imageIndex); final int height = getHeight (imageIndex); final int numSrcBands = getNumBands(imageIndex); final int dataType = streamModel.getDataType(); final int scanlineStride = Math.max(SampleModels.getScanlineStride(streamModel), width); final int bytesPerSample = SampleModels.getDataTypeSize(streamModel) / Byte.SIZE; final long bytesPerRow = (long)scanlineStride * bytesPerSample; /* * Extract user's parameters. */ final int[] sourceBands; final int[] destinationBands; final int sourceXSubsampling; final int sourceYSubsampling; if (param != null) { sourceBands = param.getSourceBands(); destinationBands = param.getDestinationBands(); sourceXSubsampling = param.getSourceXSubsampling(); sourceYSubsampling = param.getSourceYSubsampling(); } else { sourceBands = null; destinationBands = null; sourceXSubsampling = 1; sourceYSubsampling = 1; } /* * Get the stream model and the destination image. */ final int numDstBands = (destinationBands != null) ? destinationBands.length : numSrcBands; final SampleConverter[] converters = new SampleConverter[numDstBands]; final BufferedImage image = getDestination(imageIndex, param, width, height, converters); final SampleModel sampleModel = image.getSampleModel(); checkReadParamBandSettings(param, numSrcBands, sampleModel.getNumBands()); processImageStarted(imageIndex); /* * The data can be stored directly in the underlying buffer if there is no subsampling. * In order to keep this reader simpler, we also require consecutive pixels in both the * source and destination image, otherwise we will fallback on the more generic and slower * path. */ final Rectangle srcRegion = new Rectangle(); final Rectangle dstRegion = new Rectangle(); computeRegions(param, width, height, image, srcRegion, dstRegion); final int dstScanline = SampleModels.getScanlineStride(sampleModel); final int interspace = SampleModels.getDataTypeSize(sampleModel) * (sourceXSubsampling - 1) / Byte.SIZE; final boolean isRowDirect = sourceXSubsampling == 1 && SampleModels.getPixelStride(streamModel) == 1 && SampleModels.getPixelStride(sampleModel) == 1 && sampleModel.getDataType() == dataType; final boolean isDirect = isRowDirect && sourceYSubsampling == 1 && dstScanline == scanlineStride; /* * Now process to the read operation. We read each band sequentially. The values will * be either stored directly in the DataBuffer or stored using an iterator, depending * on whatever we are at least in the 'isRowDirect' case. */ final WritableRaster raster = image.getRaster(); final WritableRectIter iter; final DataBuffer buffer; final int[] offsets; if (isRowDirect) { iter = null; buffer = raster.getDataBuffer(); offsets = buffer.getOffsets(); } else { iter = RectIterFactory.createWritable(raster, dstRegion); buffer = null; offsets = null; } for (int i=0; i<numDstBands; i++) { final int srcBand = (sourceBands != null) ? sourceBands [i] : i; final int dstBand = (destinationBands != null) ? destinationBands[i] : i; final SampleConverter converter = converters[i]; /* * Computes the position for skipping all unwanted pixels in previous images, previous * bands and previous rows. The actual seek operation will be applied later. */ long position; if (input instanceof RawImageInputStream) { position = ((RawImageInputStream) input).getImageOffset(imageIndex); position += bytesPerRow * height * srcBand; } else { position = bytesPerRow * height * (numSrcBands * imageIndex + srcBand); } position += (bytesPerRow * srcRegion.y) + (bytesPerSample * srcRegion.x); /* * Optimized case: try to read all data in a single 'readFully' operation, directly * in the underlying DataBuffer. Note: there is no API for determining the bank for * a given band. But the common usage in the JDK is either one bank for all bands, * or one bank for each band. The calculation of 'bank' below assumes that this * usage apply. */ if (isRowDirect) { final int bank = (offsets.length == 1) ? 0 : dstBand; final int numReads, length; int offset = offsets[bank]; if (isDirect) { numReads = 1; length = scanlineStride * dstRegion.height; } else { numReads = dstRegion.height; length = dstRegion.width; } final float progressFactor = 100f / (numReads * numDstBands); final int progressInterval = Math.max(1, Math.round(1 / progressFactor)); for (int j=0; j<numReads; j++) { if (abortRequested()) { processReadAborted(); return image; } if ((j % progressInterval) == 0) { processImageProgress((j + i*numReads) * progressFactor); } input.seek(position); switch (dataType) { case DataBuffer.TYPE_BYTE: {byte [] array = ((DataBufferByte) buffer).getData(bank); input.readFully(array, offset, length); converter.convertUnsigned(array, offset, length); break;} case DataBuffer.TYPE_USHORT: {short [] array = ((DataBufferUShort) buffer).getData(bank); input.readFully(array, offset, length); converter.convertUnsigned(array, offset, length); break;} case DataBuffer.TYPE_SHORT: {short [] array = ((DataBufferShort) buffer).getData(bank); input.readFully(array, offset, length); converter.convert (array, offset, length); break;} case DataBuffer.TYPE_INT: {int [] array = ((DataBufferInt) buffer).getData(bank); input.readFully(array, offset, length); converter.convert (array, offset, length); break;} case DataBuffer.TYPE_FLOAT: {float [] array = ((DataBufferFloat) buffer).getData(bank); input.readFully(array, offset, length); converter.convert (array, offset, length); break;} case DataBuffer.TYPE_DOUBLE: {double[] array = ((DataBufferDouble) buffer).getData(bank); input.readFully(array, offset, length); converter.convert (array, offset, length); break;} default: throw new UnsupportedImageFormatException(Errors.format(Errors.Keys.UnsupportedDataType)); } position += bytesPerRow * sourceYSubsampling; offset += dstScanline; } continue; } /* * General case: use the RectIter. */ final float progressFactor = 100f / (dstRegion.height * numDstBands); final int progressInterval = Math.max(1, Math.round(1 / progressFactor)); int currentRow = i*dstRegion.height; iter.startBands(); for (int j=dstBand; --j>=0;) { if (iter.nextBandDone()) { throw new IIOException(Errors.format(Errors.Keys.IllegalBandNumber_1, dstBand)); } } iter.startLines(); if (!iter.finishedLines()) do { if (abortRequested()) { processReadAborted(); return image; } if ((++currentRow % progressInterval) == 0) { processImageProgress(currentRow * progressFactor); } input.seek(position); iter.startPixels(); if (!iter.finishedPixels()) do { switch (dataType) { case DataBuffer.TYPE_BYTE: iter.setSample(converter.convert(input.readUnsignedByte())); break; case DataBuffer.TYPE_USHORT: iter.setSample(converter.convert(input.readUnsignedShort())); break; case DataBuffer.TYPE_SHORT: iter.setSample(converter.convert(input.readShort())); break; case DataBuffer.TYPE_INT: iter.setSample(converter.convert(input.readInt())); break; case DataBuffer.TYPE_FLOAT: iter.setSample(converter.convert(input.readFloat())); break; case DataBuffer.TYPE_DOUBLE: iter.setSample(converter.convert(input.readDouble())); break; default: throw new UnsupportedImageFormatException(Errors.format(Errors.Keys.UnsupportedDataType)); } if (iter.nextPixelDone()) { break; } if (input.skipBytes(interspace) != interspace) { throw new EOFException(); } } while (true); position += bytesPerRow * sourceYSubsampling; } while (!iter.nextLineDone()); } processImageComplete(); return image; } /** * Service provider interface (SPI) for {@code RawImageReader}s. This SPI provides * necessary implementation for creating default {@link RawImageReader} instances. * <p> * The default constructor initializes the fields to the values listed below. * Users wanting different values should create a subclass of {@code Spi} and * set the desired values in their constructor. * <p> * <table border="1" cellspacing="0"> * <tr bgcolor="lightblue"><th>Field</th><th>Value</th></tr> * <tr><td> {@link #names}  </td><td> {@code "raw"} </td></tr> * <tr><td> {@link #MIMETypes}  </td><td> {@code "image/x-raw"} </td></tr> * <tr><td> {@link #pluginClassName}  </td><td> {@code "org.geotoolkit.image.io.plugin.RawImageReader"} </td></tr> * <tr><td> {@link #vendorName}  </td><td> {@code "Geotoolkit.org"} </td></tr> * <tr><td> {@link #version}  </td><td> Value of {@link org.geotoolkit.util.Version#GEOTOOLKIT} </td></tr> * </table> * <p> * By default, this provider register itself <em>after</em> the provider supplied by the * <cite>Image I/O extension for JAI</cite>, because the later supports a wider range of * sample models. See {@link #onRegistration onRegistration} for more information. * * @author Martin Desruisseaux (Geomatys) * @version 3.07 * * @since 3.07 (derived from 2.0) * @module */ public static class Spi extends SpatialImageReader.Spi implements SystemOverride { /** * Default list of file extensions. */ private static final String[] SUFFIXES = new String[] {"raw"}; /** * The mime types for the {@link RawImageReader}. */ private static final String[] MIME_TYPES = {"image/x-raw"}; /** * The list of valid input types. */ private static final Class<?>[] INPUT_TYPES = new Class<?>[] { RawImageInputStream.class }; /** * Constructs a default {@code RawImageReader.Spi}. The fields are initialized as * documented in the <a href="#skip-navbar_top">class javadoc</a>. Subclasses can * modify those values if desired. * <p> * For efficiency reasons, the fields are initialized to shared arrays. * Subclasses can assign new arrays, but should not modify the default array content. */ public Spi() { names = SUFFIXES; suffixes = SUFFIXES; inputTypes = INPUT_TYPES; MIMETypes = MIME_TYPES; pluginClassName = "org.geotoolkit.image.io.plugin.RawImageReader"; // This reader does not support any metadata. nativeStreamMetadataFormatName = null; nativeImageMetadataFormatName = null; } /** * Returns a brief, human-readable description of this service provider * and its associated implementation. The resulting string should be * localized for the supplied locale, if possible. * * @param locale A Locale for which the return value should be localized. * @return A String containing a description of this service provider. */ @Override public String getDescription(final Locale locale) { return Descriptions.getResources(locale).getString(Descriptions.Keys.CodecRaw); } /** * Returns {@code true} if the given source is an instance of {@link RawImageInputStream} * and is compatible with the restriction documented in {@link RawImageReader} javadoc. * * @param source The input source to be decoded. * @return {@code true} if the given source can be used by {@link RawImageReader}. * @throws IOException if an I/O error occurs while reading the stream. */ @Override public boolean canDecodeInput(final Object source) throws IOException { if (source instanceof RawImageInputStream) { final RawImageInputStream stream = (RawImageInputStream) source; final SampleModel model = stream.getImageType().getSampleModel(); if (SampleModels.getPixelStride(model) == 1) { return true; } } return false; } /** * Returns an instance of the {@code ImageReader} implementation associated * with this service provider. * * @param extension An optional extension object, which may be null. * @return An image reader instance. * @throws IOException if the attempt to instantiate the reader fails. */ @Override public ImageReader createReaderInstance(final Object extension) throws IOException { return new RawImageReader(this); } /** * Invoked when this Service Provider is registered. By default, this method * {@linkplain ServiceRegistry#setOrdering(Class, Object, Object) sets the ordering} * of this {@code RawImageReader.Spi} after the one provided in * <cite>Image I/O extension for JAI</cite>. This behavior can be changed by setting the * <code>{@value org.geotoolkit.lang.SystemOverride#KEY_ALLOW_OVERRIDE}</code> * system property explicitly to {@code true}. * <p> * Note that the Geotk RAW image reader will be selected only if the source given to the * {@link #canDecodeInput(Object)} method is compliant with the restrictions documented * in the javadoc, otherwise the standard RAW image reader will be selected instead. * * @param registry The registry where is service is registered. * @param category The category for which this service is registered. */ @Override public void onRegistration(final ServiceRegistry registry, final Class<?> category) { super.onRegistration(registry, category); if (category.equals(ImageReaderSpi.class)) { for (Iterator<ImageReaderSpi> it = registry.getServiceProviders(ImageReaderSpi.class, false); it.hasNext();) { ImageReaderSpi other = it.next(); if (other != this && ArraysExt.contains(other.getFormatNames(), "raw")) { // Found an other RAW format. For now we process only // the Sun one and leave the others (if any) untouched. if (other.getClass().getName().startsWith("com.sun.media.")) { ImageReaderSpi last = this; try { if (Boolean.getBoolean(KEY_ALLOW_OVERRIDE)) { last = other; other = this; } } catch (SecurityException e) { Logging.recoverableException(null, Spi.class, "onRegistration", e); } registry.setOrdering(ImageReaderSpi.class, other, last); } } } } } } }