/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2001-2008, Open Source Geospatial Foundation (OSGeo)
*
* 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.geotools.image.io;
import java.awt.Rectangle;
import java.awt.image.Raster;
import java.awt.image.RenderedImage;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.LogRecord;
import javax.imageio.IIOImage;
import javax.imageio.ImageWriter;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.spi.ImageWriterSpi;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataFormatImpl;
import javax.imageio.metadata.IIOInvalidTreeException;
import javax.imageio.event.IIOWriteWarningListener;
import javax.media.jai.iterator.RectIter;
import javax.media.jai.iterator.RectIterFactory;
import org.geotools.image.ImageDimension;
import org.geotools.util.logging.Logging;
import org.geotools.resources.i18n.Locales;
import org.geotools.resources.i18n.Errors;
import org.geotools.resources.i18n.ErrorKeys;
import org.geotools.resources.IndexedResourceBundle;
/**
* Base class for writers of geographic images.
*
* @since 2.4
* @source $URL$
* @version $Id$
* @author Martin Desruisseaux (IRD)
*/
public abstract class GeographicImageWriter extends ImageWriter {
/**
* The logger to use for events related to this image writer.
*/
static final Logger LOGGER = Logging.getLogger("org.geotools.image.io");
/**
* Index of the image in process of being written. This convenience index is reset to 0
* by {@link #close} method.
*/
private int imageIndex = 0;
/**
* Index of the ithumbnail in process of being written. This convenience index
* is reset to 0 by {@link #close} method.
*/
private int thumbnailIndex = 0;
/**
* Constructs a {@code GeographicImageWriter}.
*
* @param originatingProvider The {@code ImageWriterSpi} that
* is constructing this object, or {@code null}.
*/
protected GeographicImageWriter(final ImageWriterSpi provider) {
super(provider);
availableLocales = Locales.getAvailableLocales();
}
/**
* Sets the output.
*/
@Override
public void setOutput(final Object output) {
imageIndex = 0;
thumbnailIndex = 0;
super.setOutput(output);
}
/**
* Returns the resources for formatting error messages.
*/
final IndexedResourceBundle getErrorResources() {
return Errors.getResources(getLocale());
}
/**
* Ensures that the specified parameter is non-null.
*
* @param parameter The parameter name, for formatting an error message if needed.
* @param value The value to test.
* @throws IllegalArgumentException if {@code value} is null.
*/
private void ensureNonNull(final String parameter, final Object value)
throws IllegalArgumentException
{
if (value == null) {
throw new IllegalArgumentException(getErrorResources().
getString(ErrorKeys.NULL_ARGUMENT_$1, parameter));
}
}
/**
* Returns a metadata object containing default values for encoding a stream of images.
* The default implementation returns {@code null}, which is appropriate for writer that
* do not make use of stream meta-data.
*
* @param param Parameters that will be used to encode the image (in cases where
* it may affect the structure of the metadata), or {@code null}.
* @return The metadata, or {@code null}.
*/
public IIOMetadata getDefaultStreamMetadata(final ImageWriteParam param) {
return null;
}
/**
* Returns a metadata object containing default values for encoding an image of the given
* type. The default implementation returns {@code null}, which is appropriate for writer
* that do not make use of image meta-data.
*
* @param imageType The format of the image to be written later.
* @param param Parameters that will be used to encode the image (in cases where
* it may affect the structure of the metadata), or {@code null}.
* @return The metadata, or {@code null}.
*/
public IIOMetadata getDefaultImageMetadata(final ImageTypeSpecifier imageType,
final ImageWriteParam param)
{
return null;
}
/**
* Returns a metadata object initialized to the specified data for encoding a stream
* of images. The default implementation copies the specified data into a
* {@linkplain #getDefaultStreamMetadata default stream metadata}.
*
* @param inData Stream metadata used to initialize the state of the returned object.
* @param param Parameters that will be used to encode the image (in cases where
* it may affect the structure of the metadata), or {@code null}.
* @return The metadata, or {@code null}.
*/
public IIOMetadata convertStreamMetadata(final IIOMetadata inData, final ImageWriteParam param) {
ensureNonNull("inData", inData);
final IIOMetadata outData = getDefaultStreamMetadata(param);
final String format = commonMetadataFormatName(inData, outData);
if (format != null) try {
outData.mergeTree(format, inData.getAsTree(format));
return outData;
} catch (IIOInvalidTreeException ex) {
warningOccurred("convertStreamMetadata", ex);
}
return null;
}
/**
* Returns a metadata object initialized to the specified data for encoding an image
* of the given type. The default implementation copies the specified data into a
* {@linkplain #getDefaultImageMetadata default image metadata}.
*
* @param inData Image metadata used to initialize the state of the returned object.
* @param imageType The format of the image to be written later.
* @param param Parameters that will be used to encode the image (in cases where
* it may affect the structure of the metadata), or {@code null}.
* @return The metadata, or {@code null}.
*/
public IIOMetadata convertImageMetadata(final IIOMetadata inData,
final ImageTypeSpecifier imageType,
final ImageWriteParam param)
{
ensureNonNull("inData", inData);
final IIOMetadata outData = getDefaultImageMetadata(imageType, param);
final String format = commonMetadataFormatName(inData, outData);
if (format != null) try {
outData.mergeTree(format, inData.getAsTree(format));
return outData;
} catch (IIOInvalidTreeException ex) {
warningOccurred("convertImageMetadata", ex);
}
return null;
}
/**
* Finds a common metadata format name between the two specified metadata objects. This
* method query for the {@code outData} {@linkplain #getNativeMetadataFormatName native
* metadata format name} first.
*
* @param inData The input metadata.
* @param outData The output metadata.
* @return A metadata format name common to {@code inData} and {@code outData},
* or {@code null} if none where found.
*/
private static String commonMetadataFormatName(final IIOMetadata inData, final IIOMetadata outData) {
String format = outData.getNativeMetadataFormatName();
if (isSupportedFormat(inData, format)) {
return format;
}
final String[] formats = outData.getExtraMetadataFormatNames();
if (formats != null) {
for (int i=0; i<formats.length; i++) {
format = formats[i];
if (isSupportedFormat(inData, format)) {
return format;
}
}
}
if (outData.isStandardMetadataFormatSupported() && inData.isStandardMetadataFormatSupported()) {
return IIOMetadataFormatImpl.standardMetadataFormatName;
}
return null;
}
/**
* Returns {@code true} if the specified metadata supports the specified format.
*
* @param metadata The metadata to test.
* @param format The format name to test, or {@code null}.
* @return {@code true} if the specified metadata suports the specified format.
*/
private static boolean isSupportedFormat(final IIOMetadata metadata, final String format) {
if (format != null) {
if (format.equalsIgnoreCase(metadata.getNativeMetadataFormatName())) {
return true;
}
final String[] formats = metadata.getExtraMetadataFormatNames();
if (formats != null) {
for (int i=0; i<formats.length; i++) {
if (format.equalsIgnoreCase(formats[i])) {
return true;
}
}
}
}
return false;
}
/**
* Returns true if the methods that take an {@link IIOImage} parameter are capable of dealing
* with a {@link Raster}. The default implementation returns {@code true} since it is assumed
* that subclasses will fetch pixels using the iterator returned by {@link #createRectIter
* createRectIter}.
*/
@Override
public boolean canWriteRasters() {
return true;
}
/**
* Returns an iterator over the pixels of the specified image, taking subsampling in account.
*
* @param image The image or raster to be written.
* @param parameters The write parameters, or {@code null} if the whole image will be written.
* @return An iterator over the pixel values of the image to be written.
*/
protected static RectIter createRectIter(final IIOImage image, final ImageWriteParam parameters) {
/*
* Examines the parameters for subsampling in lines, columns and bands. If a subsampling
* is specified, the source region will be translated by the subsampling offset (if any).
*/
Rectangle bounds;
int[] sourceBands;
final int sourceXSubsampling;
final int sourceYSubsampling;
if (parameters != null) {
bounds = parameters.getSourceRegion(); // Needs to be a clone.
sourceXSubsampling = parameters.getSourceXSubsampling();
sourceYSubsampling = parameters.getSourceYSubsampling();
if (sourceXSubsampling != 1 || sourceYSubsampling != 1) {
if (bounds == null) {
if (image.hasRaster()) {
bounds = image.getRaster().getBounds(); // Needs to be a clone.
} else {
final RenderedImage i = image.getRenderedImage();
bounds = new Rectangle(i.getMinX(), i.getMinY(), i.getWidth(), i.getHeight());
}
}
final int xOffset = parameters.getSubsamplingXOffset();
final int yOffset = parameters.getSubsamplingYOffset();
bounds.x += xOffset;
bounds.y += yOffset;
bounds.width -= xOffset;
bounds.height -= yOffset;
// Fits to the smallest bounding box, which is
// required by SubsampledRectIter implementation.
bounds.width -= (bounds.width - 1) % sourceXSubsampling;
bounds.height -= (bounds.height - 1) % sourceYSubsampling;
}
sourceBands = parameters.getSourceBands();
} else {
sourceBands = null;
bounds = null;
sourceXSubsampling = 1;
sourceYSubsampling = 1;
}
/*
* Creates the JAIiterator which will iterates over all pixels in the source region.
* If no subsampling is specified and the source bands do not move and band, then the
* JAI iterator is returned directly.
*/
final int numBands;
RectIter iterator;
if (image.hasRaster()) {
final Raster raster = image.getRaster();
numBands = raster.getNumBands();
iterator = RectIterFactory.create(raster, bounds);
} else {
final RenderedImage raster = image.getRenderedImage();
numBands = raster.getSampleModel().getNumBands();
iterator = RectIterFactory.create(raster, bounds);
}
if (sourceXSubsampling == 1 && sourceYSubsampling == 1) {
if (sourceBands == null) {
return iterator;
}
if (sourceBands.length == numBands) {
boolean identity = true;
for (int i=0; i<numBands; i++) {
if (sourceBands[i] != i) {
identity = false;
break;
}
}
if (identity) {
return iterator;
}
}
}
/*
* A subsampling is required. Wraps the JAI iterator into a subsampler.
*/
if (sourceBands == null) {
sourceBands = new int[numBands];
for (int i=0; i<numBands; i++) {
sourceBands[i] = i;
}
}
return new SubsampledRectIter(iterator, sourceXSubsampling, sourceYSubsampling, sourceBands);
}
/**
* Computes the size of the region to be read, taking subsampling in account.
*
* @param image The image or raster to be written.
* @param parameters The write parameters, or {@code null} if the whole image will be written.
* @return dimension The dimension of the image to be written.
*/
protected static ImageDimension computeSize(final IIOImage image, final ImageWriteParam parameters) {
final ImageDimension dimension;
if (image.hasRaster()) {
dimension = new ImageDimension(image.getRaster());
} else {
dimension = new ImageDimension(image.getRenderedImage());
}
if (parameters != null) {
final Rectangle bounds = parameters.getSourceRegion();
if (bounds != null) {
if (bounds.width < dimension.width) {
dimension.width = bounds.width;
}
if (bounds.height < dimension.height) {
dimension.height = bounds.height;
}
}
dimension.width -= parameters.getSubsamplingXOffset();
dimension.height -= parameters.getSubsamplingYOffset();
}
return dimension;
}
/**
* Broadcasts the start of an image write to all registered listeners. The default
* implementation invokes the {@linkplain #processImageStarted(int) super-class method}
* with an image index maintained by this writer.
*/
protected void processImageStarted() {
processImageStarted(imageIndex);
}
/**
* Broadcasts the completion of an image write to all registered listeners.
*/
@Override
protected void processImageComplete() {
super.processImageComplete();
thumbnailIndex = 0;
imageIndex++;
}
/**
* Broadcasts the start of a thumbnail write to all registered listeners. The default
* implementation invokes the {@linkplain #processThumbnailStarted(int,int) super-class
* method} with an image and thumbnail index maintained by this writer.
*/
protected void processThumbnailStarted() {
processThumbnailStarted(imageIndex, thumbnailIndex);
}
/**
* Broadcasts the completion of a thumbnail write to all registered listeners.
*/
@Override
protected void processThumbnailComplete() {
super.processThumbnailComplete();
thumbnailIndex++;
}
/**
* Broadcasts a warning message to all registered listeners. The default implementation
* invokes the {@linkplain #processWarningOccurred(int,String) super-class method} with
* an image index maintained by this writer.
*/
protected void processWarningOccurred(final String warning) {
processWarningOccurred(imageIndex, warning);
}
/**
* Broadcasts a warning message to all registered listeners. The default implementation
* invokes the {@linkplain #processWarningOccurred(int,String,String) super-class method}
* with an image index maintained by this writer.
*/
protected void processWarningOccurred(final String baseName, final String keyword) {
processWarningOccurred(imageIndex, baseName, keyword);
}
/**
* Invoked when a warning occured. The default implementation make the following choice:
* <p>
* <ul>
* <li>If at least one {@linkplain IIOWriteWarningListener warning listener}
* has been {@linkplain #addIIOWriteWarningListener specified}, then the
* {@link IIOWriteWarningListener#warningOccurred warningOccurred} method is
* invoked for each of them and the log record is <strong>not</strong> logged.</li>
*
* <li>Otherwise, the log record is sent to the {@code "org.geotools.image.io"} logger.</li>
* </ul>
*
* Subclasses may override this method if more processing is wanted, or for
* throwing exception if some warnings should be considered as fatal errors.
*/
public void warningOccurred(final LogRecord record) {
if (warningListeners == null) {
record.setLoggerName(LOGGER.getName());
LOGGER.log(record);
} else {
processWarningOccurred(IndexedResourceBundle.format(record));
}
}
/**
* Convenience method for logging an exception from the given method.
*/
private void warningOccurred(final String method, final Exception ex) {
final LogRecord record = new LogRecord(Level.WARNING, ex.toString());
record.setSourceClassName(GeographicImageWriter.class.getName());
record.setSourceMethodName(method);
record.setThrown(ex);
warningOccurred(record);
}
/**
* To be overriden and made {@code protected} by {@link StreamImageWriter} only.
*/
void close() throws IOException {
imageIndex = 0;
thumbnailIndex = 0;
}
}