/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2001-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;
import java.awt.Rectangle;
import java.awt.image.Raster;
import java.awt.image.RenderedImage;
import java.awt.image.SampleModel;
import java.io.IOException;
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.IIOMetadataFormat;
import javax.imageio.event.IIOWriteWarningListener;
import javax.media.jai.iterator.RectIter;
import javax.media.jai.iterator.RectIterFactory;
import org.geotoolkit.image.ImageDimension;
import org.geotoolkit.image.io.metadata.SpatialMetadata;
import org.geotoolkit.image.io.metadata.SpatialMetadataFormat;
import org.geotoolkit.util.Utilities;
import org.apache.sis.util.ArraysExt;
import org.apache.sis.util.Disposable;
import org.apache.sis.util.logging.Logging;
import org.geotoolkit.resources.Errors;
import org.apache.sis.util.Locales;
import org.geotoolkit.resources.Loggings;
import org.apache.sis.util.resources.IndexedResourceBundle;
import org.geotoolkit.image.internal.ImageUtilities;
import static org.geotoolkit.image.io.SpatialImageReader.Spi.getMetadataFormatCode;
import static org.geotoolkit.image.io.metadata.SpatialMetadataFormat.GEOTK_FORMAT_NAME;
/**
* Base class for writers of spatial (usually geographic) data.
* This base class provides the following restrictions or conveniences:
* <p>
* <ul>
* <li>Set the metadata type to {@link SpatialMetadata} and its format to
* {@linkplain SpatialMetadataFormat#getStreamInstance stream metadata} and
* {@linkplain SpatialMetadataFormat#getImageInstance image metadata}.</li>
* <li>Provide {@link #getSampleModel getSampleModel}, {@link #createRectIter createRectIter}
* and {@link #computeSize computeSize} static methods as helpers for the
* {@link #write(IIOImage) write(...)} implementations.</li>
* </ul>
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @version 3.20
*
* @see SpatialImageReader
*
* @since 3.05 (derived from 2.4)
* @module
*/
public abstract class SpatialImageWriter extends ImageWriter implements WarningProducer, Disposable {
/**
* Index of the image in process of being written.
* This convenience index is reset to 0 by the {@link #close()} method.
*/
int imageIndex;
/**
* Index of the thumbnail in process of being written.
* This convenience index is reset to 0 by the {@link #close()} method.
*/
private int thumbnailIndex;
/**
* Constructs a {@code SpatialImageWriter}.
*
* @param provider The {@link ImageWriterSpi} that is constructing this object, or {@code null}.
*/
protected SpatialImageWriter(final Spi provider) {
super(provider);
availableLocales = Locales.SIS.getAvailableLocales();
}
/**
* Returns {@code true} if this writer supports the {@linkplain SpatialMetadataFormat
* spatial metadata format}. This method checks if a native or extra metadata format
* named {@value org.geotoolkit.image.io.metadata.SpatialMetadataFormat#GEOTK_FORMAT_NAME}
* is declared in the {@linkplain #originatingProvider originating provider}.
*
* @param stream {@code true} for testing stream metadata, or {@code false} for testing
* image metadata.
* @return {@code true} if the writer declares to support the spatial metadata format.
*
* @see Spi#isSpatialMetadataSupported(boolean)
*
* @since 3.20
*/
final boolean isSpatialMetadataSupported(final boolean stream) {
final ImageWriterSpi spi = originatingProvider;
return (spi instanceof Spi) && ((Spi) spi).isSpatialMetadataSupported(stream);
}
/**
* Sets the destination to the given {@link javax.imageio.stream.ImageOutputStream}
* or other {@code Object}.
*/
@Override
public void setOutput(final Object output) {
closeSilently();
super.setOutput(output);
}
/**
* Returns the resources for formatting error messages.
*/
final IndexedResourceBundle getErrorResources() {
return Errors.getResources(getLocale());
}
/**
* Returns a default parameter object appropriate for this format. The default
* implementation constructs and returns a new {@link SpatialImageWriteParam}.
*
* @return An {@code ImageWriteParam} object which may be used.
*/
@Override
public SpatialImageWriteParam getDefaultWriteParam() {
return new SpatialImageWriteParam(this);
}
/**
* Returns a metadata object containing default values for encoding a stream of images.
* The default implementation returns an instance of {@link SpatialMetadata} using
* the {@linkplain SpatialMetadataFormat#getStreamInstance(String) stream format} if the
* {@linkplain #getOriginatingProvider() originating provider} declares supporting that
* format, or {@code null} otherwise.
*
* @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}.
*/
@Override
public SpatialMetadata getDefaultStreamMetadata(final ImageWriteParam param) {
if (!isSpatialMetadataSupported(true)) {
return null;
}
return new SpatialMetadata(true, this, null);
}
/**
* Returns a metadata object containing default values for encoding an image of the given
* type. The default implementation returns an instance of {@link SpatialMetadata} using
* the {@linkplain SpatialMetadataFormat#getImageInstance(String) image format} if the
* {@linkplain #getOriginatingProvider() originating provider} declares supporting that
* format, or {@code null} otherwise.
*
* @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}.
*/
@Override
public SpatialMetadata getDefaultImageMetadata(final ImageTypeSpecifier imageType,
final ImageWriteParam param)
{
if (!isSpatialMetadataSupported(false)) {
return null;
}
return new SpatialMetadata(false, this, null);
}
/**
* Returns a metadata object initialized to the specified data for encoding a stream of
* images. The default implementation returns the given metadata unchanged if it is an
* instance of {@link SpatialMetadata} using the
* {@linkplain SpatialMetadataFormat#getStreamInstance(String) stream format},
* or wraps it otherwise.
*
* @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}.
*/
@Override
public SpatialMetadata convertStreamMetadata(final IIOMetadata inData,
final ImageWriteParam param)
{
if (inData == null) {
return null;
}
final SpatialMetadataFormat format = SpatialMetadataFormat.getStreamInstance(GEOTK_FORMAT_NAME);
if (inData instanceof SpatialMetadata) {
final SpatialMetadata sp = (SpatialMetadata) inData;
if (format.equals(sp.format)) {
return sp;
}
}
return new SpatialMetadata(format, this, inData);
}
/**
* Returns a metadata object initialized to the specified data for encoding an image of the
* given type. The default implementation returns the given metadata unchanged if it is an
* instance of {@link SpatialMetadata} using the
* {@linkplain SpatialMetadataFormat#getImageInstance(String) image format},
* or wraps it otherwise.
*
* @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}.
*/
@Override
public SpatialMetadata convertImageMetadata(final IIOMetadata inData,
final ImageTypeSpecifier imageType,
final ImageWriteParam param)
{
if (inData == null) {
return null;
}
final SpatialMetadataFormat format = SpatialMetadataFormat.getImageInstance(GEOTK_FORMAT_NAME);
if (inData instanceof SpatialMetadata) {
final SpatialMetadata sp = (SpatialMetadata) inData;
if (format.equals(sp.format)) {
return sp;
}
}
return new SpatialMetadata(format, this, inData);
}
/**
* 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(IIOImage, ImageWriteParam)}.
*/
@Override
public boolean canWriteRasters() {
return true;
}
/**
* Returns the sample model to use for the destination image to be written. Note that the
* {@linkplain SampleModel#getWidth() width} and {@linkplain SampleModel#getHeight() height}
* of the returned sample model are usually <strong>not</strong> valid, because they have
* not been adjusted for the source or destination regions.
*
* @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 The sample model of the destination image.
*
* @since 3.07
*/
protected static SampleModel getSampleModel(final IIOImage image, final ImageWriteParam parameters) {
if (parameters != null) {
final ImageTypeSpecifier type = parameters.getDestinationType();
if (type != null) {
return type.getSampleModel();
}
}
if (image.hasRaster()) {
return image.getRaster().getSampleModel();
}
return image.getRenderedImage().getSampleModel();
}
/**
* 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 {
bounds = ImageUtilities.getBounds(image.getRenderedImage());
}
}
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 JAI iterator which will iterate 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 written, 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) {
int width = dimension.width;
int height = dimension.height;
final Rectangle bounds = parameters.getSourceRegion();
if (bounds != null) {
if (bounds.width < width) {
width = bounds.width;
}
if (bounds.height < height) {
height = bounds.height;
}
}
final int sourceXSubsampling = parameters.getSourceXSubsampling();
final int sourceYSubsampling = parameters.getSourceYSubsampling();
width -= parameters.getSubsamplingXOffset();
height -= parameters.getSubsamplingYOffset();
width = (width + sourceXSubsampling-1) / sourceXSubsampling;
height = (height + sourceYSubsampling-1) / sourceYSubsampling;
dimension.setSize(width, height);
}
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.
*
* @param warning The warning message to broadcasts.
*/
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.
*
* @param baseName The base name of a set of {@code ResourceBundle}s
* containing localized warning messages.
* @param keyword The keyword used to index the warning message within the set of
* {@code ResourceBundle}s.
*/
protected void processWarningOccurred(final String baseName, final String keyword) {
processWarningOccurred(imageIndex, baseName, keyword);
}
/**
* Invoked when a warning occurred. The default implementation makes 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.geotoolkit.image.io"} logger.</li>
* </ul>
* <p>
* Subclasses may override this method if more processing is wanted, or for
* throwing exception if some warnings should be considered as fatal errors.
*
* @param record The warning record to log.
* @return {@code true} if the message has been sent to at least one warning listener,
* or {@code false} if it has been sent to the logging system as a fallback.
*
* @see org.geotoolkit.image.io.metadata.MetadataNodeAccessor#warningOccurred(LogRecord)
*/
@Override
public boolean warningOccurred(final LogRecord record) {
if (warningListeners == null) {
record.setLoggerName(LOGGER.getName());
LOGGER.log(record);
return false;
} else {
processWarningOccurred(Loggings.format(record));
return true;
}
}
/**
* Invokes {@link #close} and logs the exception if any. This method is invoked from
* methods that do not allow {@link IOException} to be thrown. Since we will not use
* the stream anymore after closing it, it should not be a big deal if an error occurred.
*/
final void closeSilently() {
try {
close();
} catch (IOException exception) {
Logging.unexpectedException(LOGGER, getClass(), "close", exception);
}
}
/**
* Invoked when a new output is set or when the writer is disposed. The default implementation
* clears the internal cache. Sub-classes can override this method if they have more resources
* to dispose, but should always invoke {@code super.close()}.
* <p>
* This method is overridden and given {@code protected} access by {@link StreamImageWriter}
* and {@link ImageWriterAdapter}. It is called "{@code close}" in order to match the
* purpose which appear in the public API of those classes.
*
* @throws IOException If an error occurred while closing a stream (applicable to subclasses only).
*/
void close() throws IOException {
imageIndex = 0;
thumbnailIndex = 0;
}
/*
* There is no need to override reset(), because the default ImageReader.reset()
* implementation invokes setInput(null, false, false), which in turn invokes our
* closeSilently() method.
*/
/**
* Allows any resources held by this writer to be released. If an output stream were
* created by {@link StreamImageWriter} or {@link ImageWriterAdapter}, it will be
* {@linkplain StreamImageWriter#close() closed} before to dispose this reader.
*/
@Override
public void dispose() {
closeSilently();
super.dispose();
}
/**
* Service provider interfaces (SPI) for {@link SpatialImageWriter}s.
* This base class initializes fields to the values listed below:
* <p>
* <table border="1">
* <tr bgcolor="lightblue">
* <th>Field</th>
* <th>Value</th>
* </tr><tr>
* <td> {@link #nativeStreamMetadataFormatName} </td>
* <td> {@value org.geotoolkit.image.io.metadata.SpatialMetadataFormat#GEOTK_FORMAT_NAME} </td>
* </tr><tr>
* <td> {@link #nativeImageMetadataFormatName} </td>
* <td> {@value org.geotoolkit.image.io.metadata.SpatialMetadataFormat#GEOTK_FORMAT_NAME} </td>
* </tr>
* </table>
* <p>
* All other fields are left to their default values ({@code null} or {@code false}).
* Subclasses are responsible for initializing those fields.
* Some subclasses may also restore the {@link #nativeStreamMetadataFormatName} to
* {@code null} if they do not support stream metadata.
*
* @author Martin Desruisseaux (Geomatys)
* @version 3.07
*
* @see SpatialImageReader.Spi
*
* @since 3.07
* @module
*/
protected abstract static class Spi extends ImageWriterSpi {
/**
* Initializes a default provider for {@link SpatialImageWriter}s.
* <p>
* For efficiency reasons, the fields are initialized to a shared array.
* Subclasses can assign new arrays, but should not modify the default array content.
*/
protected Spi() {
nativeStreamMetadataFormatName = GEOTK_FORMAT_NAME;
nativeImageMetadataFormatName = GEOTK_FORMAT_NAME;
if (getClass().getName().startsWith("org.geotoolkit.")) {
vendorName = "Geotoolkit.org";
version = Utilities.VERSION.toString();
}
}
/**
* Adds the given format to the list of extra stream or metadata format names,
* if not already present. This method does nothing if the format is already
* listed as the native or an extra format.
*
* @param formatName
* @param stream {@code true} for adding to the list of {@linkplain #extraStreamMetadataFormatNames extra stream formats}.
* @param image {@code true} for adding to the list of {@linkplain #extraImageMetadataFormatNames extra image formats}.
*
* @since 3.20
*/
protected void addExtraMetadataFormat(final String formatName, final boolean stream, final boolean image) {
if (stream) extraStreamMetadataFormatNames = ImageReaderAdapter.Spi.addExtraMetadataFormat(formatName, nativeStreamMetadataFormatName, extraStreamMetadataFormatNames);
if (image) extraImageMetadataFormatNames = ImageReaderAdapter.Spi.addExtraMetadataFormat(formatName, nativeImageMetadataFormatName, extraImageMetadataFormatNames);
}
/**
* Returns {@code true} if this provider supports the {@linkplain SpatialMetadataFormat
* spatial metadata format}. This method checks if a native or extra metadata format
* named {@value org.geotoolkit.image.io.metadata.SpatialMetadataFormat#GEOTK_FORMAT_NAME}
* is declared. This is the case by default unless subclasses modified the values of
* the {@code xxxFormatName} fields.
*
* @param stream {@code true} for testing stream metadata, or {@code false} for testing
* image metadata.
* @return {@code true} if the provider declares to support the spatial metadata format.
*
* @see SpatialImageWriter#isSpatialMetadataSupported(boolean)
*/
final boolean isSpatialMetadataSupported(final boolean stream) {
final String nativeFormat;
final String[] extraFormats;
if (stream) {
nativeFormat = nativeStreamMetadataFormatName;
extraFormats = extraStreamMetadataFormatNames;
} else {
nativeFormat = nativeImageMetadataFormatName;
extraFormats = extraImageMetadataFormatNames;
}
return GEOTK_FORMAT_NAME.equals(nativeFormat) || ArraysExt.contains(extraFormats, GEOTK_FORMAT_NAME);
}
/**
* Returns a description of the stream metadata of the given name.
* If no description is available, then this method returns {@code null}.
*
* @param formatName The desired stream metadata format.
* @return The stream metadata format of the given name.
*/
@Override
public IIOMetadataFormat getStreamMetadataFormat(final String formatName) {
switch (getMetadataFormatCode(formatName,
nativeStreamMetadataFormatName,
nativeStreamMetadataFormatClassName,
extraStreamMetadataFormatNames,
extraStreamMetadataFormatClassNames))
{
case 0: return null;
case 1: return SpatialMetadataFormat.getStreamInstance(formatName);
default: return super.getStreamMetadataFormat(formatName);
}
}
/**
* Returns a description of the image metadata of the given name.
* If no description is available, then this method returns {@code null}.
*
* @param formatName The desired image metadata format.
* @return The image metadata format of the given name.
*/
@Override
public IIOMetadataFormat getImageMetadataFormat(final String formatName) {
switch (getMetadataFormatCode(formatName,
nativeImageMetadataFormatName,
nativeImageMetadataFormatClassName,
extraImageMetadataFormatNames,
extraImageMetadataFormatClassNames))
{
case 0: return null;
case 1: return SpatialMetadataFormat.getImageInstance(formatName);
default: return super.getImageMetadataFormat(formatName);
}
}
}
}