/* * Geotoolkit.org - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2010-2012, Open Source Geospatial Foundation (OSGeo) * (C) 2010-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.coverage.io; import java.util.Set; import java.util.Map; import java.util.HashMap; import java.util.List; import java.util.Iterator; import java.util.Collections; import java.util.concurrent.CancellationException; import javax.imageio.ImageReader; import javax.imageio.ImageReadParam; import javax.imageio.ImageTypeSpecifier; import javax.imageio.spi.ImageReaderSpi; import javax.imageio.IIOException; import java.io.IOException; import java.awt.Dimension; import java.awt.Rectangle; import java.awt.image.ColorModel; import java.awt.image.SampleModel; import java.awt.image.BufferedImage; import java.awt.image.RenderedImage; import javax.media.jai.PlanarImage; import javax.media.jai.RenderedImageAdapter; import org.opengis.coverage.grid.GridCoverage; import org.opengis.coverage.grid.GridEnvelope; import org.opengis.referencing.datum.PixelInCell; import org.opengis.referencing.operation.TransformException; import org.apache.sis.util.logging.Logging; import org.apache.sis.util.NullArgumentException; import org.apache.sis.geometry.Envelopes; import org.apache.sis.geometry.GeneralEnvelope; import org.geotoolkit.image.io.SpatialImageReader; import org.geotoolkit.image.io.metadata.SpatialMetadata; import org.geotoolkit.coverage.GridSampleDimension; import org.geotoolkit.coverage.grid.GeneralGridGeometry; import org.geotoolkit.resources.Errors; import static org.geotoolkit.image.io.MultidimensionalImageStore.*; /** * An {@link ImageReader} implementation which use a {@link GridCoverageReader} for reading * sample values. This class is the converse of {@link ImageCoverageReader}: it takes a high * level construct ({@code GridCoverageReader}) and wraps it as a lower level construct * ({@code ImageReader}). This is an unusual thing to do - consequently, the only purpose * of this class is to allow usage of an existing {@code GridCoverageReader} instance with * API working only with {@code ImageReader} instances. * * {@section Example} * {@code ImageReaderAdapter} can be used in order to show the content of a {@code GridCoverageReader} * in an {@link org.geotoolkit.gui.swing.image.ImageFileProperties} widget. * * {@note An other approach would be to unwrap the <code>ImageReader</code> which is wrapped * by <code>ImageCoverageReader</code>. However the result would not be always the same, * because some modules (for example <cite>geotk-coverage-sql</cite>) define subclasses * of <code>ImageCoverageReader</code> which alter the way the image is read. The purpose * of <code>ImageReaderAdapter</code> is to get exactly the same image than the one produced * by the wrapped <code>GridCoverageReader</code>.} * * @author Martin Desruisseaux (Geomatys) * @version 3.19 * * @since 3.14 * @module */ public class ImageReaderAdapter extends SpatialImageReader { /** * The sample dimension to make visible. Declared as a constant in order to spot * the places where the value {@value} is assumed. * * @see org.geotoolkit.coverage.AbstractCoverage#VISIBLE_BAND */ private static final int VISIBLE_BAND = 0; /** * The coverage reader on which to delegate all {@code ImageReader} method invocations. */ protected final GridCoverageReader reader; /** * The number of images, or 0 if not yet computed. */ private int numImages; /** * The image sizes at various image indices. Values are computed from the grid geometries * when first needed, and cached because each value is typically fetched twice (once for * image width, and once for image height). */ private final Map<Integer,Dimension> imageSizes = new HashMap<>(); /** * The image types at various image indices. Values are computed from the sample * dimensions when first needed. */ private final Map<Integer,ImageTypeSpecifier> imageTypes = new HashMap<>(); /** * Creates a new adapter for the given coverage reader. * * @param reader The coverage reader on which to delegate all * {@code ImageReader} method invocations. */ public ImageReaderAdapter(final GridCoverageReader reader) { super(null); if (reader == null) { throw new NullArgumentException(Errors.format(Errors.Keys.NullArgument_1, "reader")); } this.reader = reader; } /** * Returns the {@link ImageReader} provider, or {@code null} if none. */ @Override public ImageReaderSpi getOriginatingProvider() { if (reader instanceof ImageCoverageReader) { final ImageReader ir = ((ImageCoverageReader) reader).imageReader; if (ir != null) { return ir.getOriginatingProvider(); } } return super.getOriginatingProvider(); } /** * Converts the given {@link CoverageStoreException} to an {@link IOException}. This method * unwraps the {@code IOException} if the {@code CoverageStoreException} was just a wrapper * for the former. Otherwise the {@code CoverageStoreException} is wrapped in a new * {@link IIOException}. */ private static IOException convert(final CoverageStoreException exception) { final Throwable cause = exception.getCause(); if (cause instanceof IOException) { return (IOException) cause; } return new IIOException(exception.getLocalizedMessage(), exception); } /** * Sets the input to be read. In the input can be successfully assigned to the wrapped * {@link GridCoverageReader}, then it is also saved in the inherited {@link #input} field * for retrieval by {@link #getInput()}. */ @Override public void setInput(final Object input, boolean seekForwardOnly, boolean ignoreMetadata) { this.input = null; numImages = 0; imageSizes.clear(); imageTypes.clear(); super.dispose(); // Must be the super-class method, not this.dispose(). try { reader.setInput(input); } catch (CoverageStoreException e) { throw new IllegalArgumentException(e); } this.input = input; // Do not invoke super.setInput(...). this.seekForwardOnly = seekForwardOnly; // Saved as a matter of principle, but not used. this.ignoreMetadata = ignoreMetadata; } /** * Returns the number of images in the current input. The default implementation returns * the length of the list returned by {@link GridCoverageReader#getCoverageNames()}. * * @throws IOException if an error occurs reading the information from the input source. */ @Override public int getNumImages(final boolean allowSearch) throws IOException { if (numImages == 0) try { numImages = reader.getCoverageNames().size(); } catch (CoverageStoreException e) { throw convert(e); } return numImages; } /** * Returns the number of bands available for the specified image. The default implementation * returns the length of the list returned by {@link GridCoverageReader#getSampleDimensions(int)}. * * @throws IOException if an error occurs reading the information from the input source. */ @Override public int getNumBands(final int imageIndex) throws IOException { final List<GridSampleDimension> sampleDimensions; try { sampleDimensions = reader.getSampleDimensions(imageIndex); } catch (CoverageStoreException e) { throw convert(e); } return (sampleDimensions != null) ? sampleDimensions.size() : 1; } /** * Returns the number of dimension of the image at the given index. The default * implementation returns the dimension of the geometry returned by * {@link GridCoverageReader#getGridGeometry(int)}. * * @throws IOException if an error occurs reading the information from the input source. */ @Override public int getDimension(final int imageIndex) throws IOException { try { return reader.getGridGeometry(imageIndex).getDimension(); } catch (CoverageStoreException e) { throw convert(e); } } /** * Returns the grid envelope of the image at the given index. The default * implementation returns the grid range of the geometry returned by * {@link GridCoverageReader#getGridGeometry(int)}. * * @throws IOException if an error occurs reading the information from the input source. * * @since 3.19 */ @Override public GridEnvelope getGridEnvelope(final int imageIndex) throws IOException { try { return reader.getGridGeometry(imageIndex).getExtent(); } catch (CoverageStoreException e) { throw convert(e); } } /** * Returns the image width and height at the given index. The default implementation computes * the size from the geometry returned by {@link GridCoverageReader#getGridGeometry(int)}. * Subclasses can override this method if they want to compute the size in a different way. * * @param imageIndex The image index. * @return The width and height of the image at the given index. * @throws IOException if an error occurs reading the information from the input source. */ protected Dimension getSize(final int imageIndex) throws IOException { final Integer key = imageIndex; Dimension size = imageSizes.get(key); if (size == null) { final GeneralGridGeometry geometry; try { geometry = reader.getGridGeometry(imageIndex); } catch (CoverageStoreException e) { throw convert(e); } final GridEnvelope range = geometry.getExtent(); size = new Dimension(range.getSpan(X_DIMENSION), range.getSpan(Y_DIMENSION)); imageSizes.put(key, size); } return size; } /** * Returns the width of the image at the given index. This method delegates * to {@link #getSize(int)}, which computes the size from the grid geometry. * * @throws IOException if an error occurs reading the information from the input source. */ @Override public final int getWidth(final int imageIndex) throws IOException { return getSize(imageIndex).width; } /** * Returns the height of the image at the given index. This method delegates * to {@link #getSize(int)}, which computes the size from the grid geometry. * * @throws IOException if an error occurs reading the information from the input source. */ @Override public final int getHeight(final int imageIndex) throws IOException { return getSize(imageIndex).height; } /** * Returns the data type which most closely represents the "raw" internal data of the image. * The default implementation returns the data type of the sample model of the type returned * by {@link #getRawImageType(int)}. * * @throws IOException if an error occurs reading the information from the input source. */ @Override protected int getRawDataType(final int imageIndex) throws IOException { final ImageTypeSpecifier type = getRawImageType(imageIndex); return (type != null) ? type.getSampleModel().getDataType() : super.getRawDataType(imageIndex); } /** * Returns the raw type of the image at the given index. The default implementation computes * the type from the value returned by {@link GridCoverageReader#getSampleDimensions(int)}. * * @throws IOException if an error occurs reading the information from the input source. */ @Override public ImageTypeSpecifier getRawImageType(final int imageIndex) throws IOException { final Integer key = imageIndex; ImageTypeSpecifier type = imageTypes.get(key); if (type == null) { final List<GridSampleDimension> bands; try { bands = reader.getSampleDimensions(imageIndex); } catch (CoverageStoreException e) { throw convert(e); } if (bands != null) { final int numBands = bands.size(); if (numBands > VISIBLE_BAND) { final Dimension size = getSize(imageIndex); final ColorModel cm = bands.get(VISIBLE_BAND).getColorModel(VISIBLE_BAND, bands.size()); final SampleModel sm = cm.createCompatibleSampleModel(size.width, size.height); type = new ImageTypeSpecifier(cm, sm); } } imageTypes.put(key, type); } return type; } /** * Returns the possible image types to which the given image can be decoded. The default * implementation puts the value returned by {@link #getRawDataType(int)} in a singleton set. * * @throws IOException if an error occurs reading the information from the input source. */ @Override public Iterator<ImageTypeSpecifier> getImageTypes(final int imageIndex) throws IOException { final ImageTypeSpecifier type = getRawImageType(imageIndex); final Set<ImageTypeSpecifier> types; if (type != null) { types = Collections.singleton(type); } else { types = Collections.emptySet(); } return types.iterator(); } /** * Fetches the stream metadata or image metadata. This method is invoked automatically when * the metadata are requested for the first time. The default implementation delegates directly * to the coverage reader. * * @throws IOException if an error occurs reading the information from the input source. */ @Override protected SpatialMetadata createMetadata(final int imageIndex) throws IOException { try { return (imageIndex < 0) ? reader.getStreamMetadata() : reader.getCoverageMetadata(imageIndex); } catch (CoverageStoreException e) { throw convert(e); } } /** * Reads the image at the given index. The default implementation reads the coverage using * the wrapped {@link GridCoverageReader}, then extracts the {@link RenderedImage} from * the coverage. Note that the image returned by this method will typically be an instance * of {@link PlanarImage} rather than {@link BufferedImage}. * * @throws IOException if an error occurs reading the information from the input source. */ @Override public RenderedImage readAsRenderedImage(final int imageIndex, final ImageReadParam param) throws IOException { GridCoverageReadParam gp = null; if (param != null) { gp = new GridCoverageReadParam(); gp.setSourceBands(param.getSourceBands()); gp.setDestinationBands(param.getDestinationBands()); /* * Computes the geodetic envelope. */ final GeneralGridGeometry geometry; try { geometry = reader.getGridGeometry(imageIndex); } catch (CoverageStoreException e) { throw convert(e); } final GridEnvelope range = geometry.getExtent(); final Rectangle srcRect = new Rectangle(); final Rectangle dstRect = new Rectangle(); // Required but ignored. computeRegions(param, range.getSpan(X_DIMENSION), range.getSpan(Y_DIMENSION), null, srcRect, dstRect); GeneralEnvelope region = new GeneralEnvelope(range.getDimension()); for (int i=region.getDimension(); --i >= 0;) { final double min, max; switch (i) { case X_DIMENSION: min=srcRect.getMinX(); max=srcRect.getMaxX(); break; case Y_DIMENSION: min=srcRect.getMinY(); max=srcRect.getMaxY(); break; default: min=0; max=1; break; } region.setRange(i, min, max); } try { region = Envelopes.transform(geometry.getGridToCRS(PixelInCell.CELL_CORNER), region); } catch (TransformException e) { throw new IIOException(e.getLocalizedMessage(), e); } gp.setEnvelope(region); /* * Computes the resolution. */ final int xSubsampling = param.getSourceXSubsampling(); final int ySubsampling = param.getSourceYSubsampling(); if (xSubsampling != 1 || ySubsampling != 1) { final double[] resolution = new double[region.getDimension()]; resolution[X_DIMENSION] = xSubsampling * region.getSpan(X_DIMENSION) / srcRect.getWidth(); resolution[Y_DIMENSION] = ySubsampling * region.getSpan(Y_DIMENSION) / srcRect.getHeight(); gp.setResolution(resolution); } } final GridCoverage coverage = read(imageIndex, gp); return (coverage == null) ? null : coverage.getRenderableImage(X_DIMENSION, Y_DIMENSION).createDefaultRendering(); } /** * Reads the image at the given index. The default implementation delegates to * {@link #readAsRenderedImage(int, ImageReadParam)}, then converts the image * to an instance of {@link BufferedImage}. * <p> * The {@code readAsRenderedImage} method should be preferred when the image is * not required to be an instance of {@code BufferedImage}. * * @throws IOException if an error occurs reading the information from the input source. */ @Override public BufferedImage read(final int imageIndex, final ImageReadParam param) throws IOException { RenderedImage image = readAsRenderedImage(imageIndex, param); while (image instanceof RenderedImageAdapter) { image = ((RenderedImageAdapter) image).getWrappedImage(); } if (image instanceof PlanarImage) { return ((PlanarImage) image).getAsBufferedImage(); } return (BufferedImage) image; } /** * Reads the coverage at the given index. This method is invoked by {@link #readAsRenderedImage(int, * ImageReadParam) readAsRenderedImage} after the <cite>image parameters</cite> have been converted * to <cite>coverage parameters</cite>. * <p> * The default implementation delegates to * {@link GridCoverageReader#read(int, GridCoverageReadParam)}. Subclasses can override * this method if they want to perform additional processing before or after the coverage * is read. For example a subclass may invoke {@code GridCoverage2D.view(ViewType.RENDERED)} * for a better image rendering. * * @param index The index of the coverage to be queried. * @param param Optional parameters used to control the reading process, or {@code null}. * @return The {@link GridCoverage} at the specified index, or {@code null} if {@link #abort()} * has been invoked in an other thread during the execution of this method. * @throws IOException If the coverage can not be read. */ protected GridCoverage read(final int index, final GridCoverageReadParam param) throws IOException { try { return reader.read(index, param); } catch (CoverageStoreException e) { throw convert(e); } catch (CancellationException e) { return null; } } /** * Aborts the current reading process. This method forward the call to the wrapped * {@link GridCoverageReader}, but does not set the {@link #abortRequested} flag in * this class (because it is not used). */ @Override public void abort() { reader.abort(); } /** * Disposes this image reader and the wrapped {@link GridCoverageReader}. */ @Override public void dispose() { try { reader.dispose(); } catch (CoverageStoreException e) { Logging.unexpectedException(null, getClass(), "dispose", e); } super.dispose(); } }