/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 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.mosaic; import java.awt.Dimension; import java.awt.Rectangle; import java.awt.image.Raster; import java.awt.image.DataBuffer; import java.awt.image.SampleModel; import java.awt.image.RenderedImage; import java.awt.image.BufferedImage; import java.io.File; import java.io.Closeable; import java.io.IOException; import javax.imageio.*; // Lot of them in this class. import javax.imageio.spi.IIORegistry; import javax.imageio.spi.ImageReaderSpi; import javax.imageio.spi.ImageWriterSpi; import javax.imageio.metadata.IIOMetadata; import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.ImageOutputStream; import java.util.*; // Lot of them in this class. import java.util.logging.Level; import java.util.logging.Logger; import java.util.logging.LogRecord; import java.util.concurrent.Future; import java.util.concurrent.Callable; import java.util.concurrent.Executors; import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutionException; import java.lang.reflect.UndeclaredThrowableException; import org.geotools.factory.GeoTools; import org.geotools.image.io.ImageIOExt; import org.geotools.resources.XArray; import org.geotools.resources.i18n.Errors; import org.geotools.resources.i18n.ErrorKeys; import org.geotools.resources.i18n.Loggings; import org.geotools.resources.i18n.LoggingKeys; import org.geotools.resources.i18n.Vocabulary; import org.geotools.resources.i18n.VocabularyKeys; import org.geotools.resources.image.ImageUtilities; import org.geotools.resources.IndexedResourceBundle; import org.geotools.util.logging.Logging; /** * An image writer that delegates to a mosaic of other image writers. The mosaic is specified as a * collection of {@link Tile} objects given to {@link #setOutput(Object)} method. While this class * can write a {@link RenderedImage}, the preferred approach is to invoke {@link #writeFromInput} * with a {@link File} input in argument. This approach is non-standard but often required since * images to mosaic are typically bigger than {@link RenderedImage} capacity. * * @since 2.5 * * @source $URL$ * @version $Id$ * @author Cédric Briançon * @author Martin Desruisseaux */ public class MosaicImageWriter extends ImageWriter { /** * The value for filling empty images. The value is fixed to 0 in current implementation * because this is the value of newly created image, and we do not fill them at this time. */ private static final int FILL_VALUE = 0; /** * The logging level for tiling information during reads and writes. */ private Level level = Level.FINE; /** * The executor to use for writting tiles in background thread. * Will be created when first needed. */ private ExecutorService executor; /** * Constructs an image writer with the default provider. */ public MosaicImageWriter() { this(null); } /** * Constructs an image writer with the specified provider. * * @param spi The service provider, or {@code null} for the default one. */ public MosaicImageWriter(final ImageWriterSpi spi) { super(spi != null ? spi : Spi.DEFAULT); } /** * Returns the logging level for tile information during read and write operations. * * @return The current logging level. */ public Level getLogLevel() { return level; } /** * Sets the logging level for tile information during read and write operations. * The default value is {@link Level#FINE}. A {@code null} value restore the default. * * @param level The new logging level, or {@code null} for the default. */ public void setLogLevel(Level level) { if (level == null) { level = Level.FINE; } this.level = level; } /** * Returns the output, which is a an array of {@linkplain TileManager tile managers}. * The array length is the maximum number of images that can be inserted. The element * at index <var>i</var> is the tile manager to use when writing at image index <var>i</var>. */ @Override public TileManager[] getOutput() { final TileManager[] managers = (TileManager[]) super.getOutput(); return (managers != null) ? managers.clone() : null; } /** * Sets the output, which is expected to be an array of {@linkplain TileManager tile managers}. * If the given input is a singleton, an array or a {@linkplain Collection collection} of * {@link Tile} objects, then it will be wrapped in an array of {@link TileManager}s. * * @param output The output. * @throws IllegalArgumentException if {@code output} is not an instance of one of the * expected classes, or if the output can not be used because of an I/O error * (in which case the exception has a {@link IOException} as its * {@linkplain IllegalArgumentException#getCause cause}). */ @Override public void setOutput(final Object output) throws IllegalArgumentException { final TileManager[] managers; try { managers = TileManagerFactory.DEFAULT.createFromObject(output); } catch (IOException e) { throw new IllegalArgumentException(e.getLocalizedMessage(), e); } super.setOutput(managers); } /** * Returns default parameters appropriate for this format. */ @Override public MosaicImageWriteParam getDefaultWriteParam() { return new MosaicImageWriteParam(); } /** * Writes the specified image as a set of tiles. The default implementation copies the image in * a temporary file, then invokes {@link #writeFromInput}. This somewhat inefficient approach * may be changed in a future version. * * @param metadata The stream metadata. * @param image The image to write. * @param param The parameter for the image to write. * @throws IOException if an error occured while writing the image. */ public void write(final IIOMetadata metadata, final IIOImage image, ImageWriteParam param) throws IOException { /* * We could check for 'output' before to create the temporary file in order to avoid * creating the file if we are going to fail anyway, but we don't because users are * allowed to override the 'filter' methods and set the output there (undocumented * but possible, and TileBuilder do something like that). * * Uses the PNG format, which is lossless and bundled in standard Java distributions. */ final Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("png"); while (writers.hasNext()) { final ImageWriter writer = writers.next(); if (!filter(writer)) { continue; } final File file = File.createTempFile("MIW", ".png"); try { final ImageOutputStream output = ImageIOExt.createImageOutputStream(image.getRenderedImage(), file); writer.setOutput(output); writer.write(metadata, image, param); output.close(); /* * We don't want to take in account parameters like source region, subsampling, etc. * since they were already handled by the writing process above. But we want to take * in account the parameters specific to MosaicImageWriteParam. So retain only them. */ if (param instanceof MosaicImageWriteParam) { param = new MosaicImageWriteParam((MosaicImageWriteParam) param); } else { param = null; } writeFromInput(file, 0, param); } finally { file.delete(); } return; } throw new IIOException(Errors.format(ErrorKeys.NO_IMAGE_WRITER)); } /** * Reads the image from the given input and writes it as a set of tiles. The input is typically * a {@link File} object, but other kind of inputs may be accepted depending on available image * readers. The output files and tiling layout can be specified as a collection of {@link Tile} * objects given to {@link #setOutput(Object)} method. * * @param input The image input, typically as a {@link File}. * @param inputIndex The image index to read from the given input file. * @param param The write parameters, or {@code null} for the default. * @return {@code true} on success, or {@code false} if the process has been aborted. * @throws IOException If an error occured while reading or writing. * * @todo Current implementation do not yet supports source region and subsampling settings. * An exception will be thrown if any of those parameters are set. */ public boolean writeFromInput(final Object input, final int inputIndex, final ImageWriteParam param) throws IOException { final boolean success; final ImageReader reader = getImageReader(input); try { success = writeFromReader(reader, inputIndex, param); close(reader.getInput(), input); } finally { reader.dispose(); } return success; } /** * Reads the image from the given reader and writes it as a set of tiles. * It is the caller responsability to dispose the reader when writing is done. */ private boolean writeFromReader(final ImageReader reader, final int inputIndex, final ImageWriteParam writeParam) throws IOException { clearAbortRequest(); final int outputIndex; final TileWritingPolicy policy; if (writeParam instanceof MosaicImageWriteParam) { final MosaicImageWriteParam param = (MosaicImageWriteParam) writeParam; outputIndex = param.getOutputIndex(); policy = param.getTileWritingPolicy(); } else { outputIndex = 0; policy = TileWritingPolicy.OVERWRITE; } processImageStarted(outputIndex); /* * Gets the reader first - especially before getOutput() - because the user may have * overriden filter(ImageReader) and set the output accordingly. TileBuilder do that. */ final TileManager[] managers = getOutput(); if (managers == null) { throw new IllegalStateException(Errors.format(ErrorKeys.NO_IMAGE_OUTPUT)); } final List<Tile> tiles; final int bytesPerPixel; if (policy.equals(TileWritingPolicy.NO_WRITE)) { tiles = Collections.emptyList(); bytesPerPixel = 1; } else { tiles = new LinkedList<Tile>(managers[outputIndex].getTiles()); /* * Computes an estimation of the amount of memory to be required for each pixel. * This estimation may not be accurate especially for image packing many pixels * per byte, but a value too high is probably better than a value too low. */ final SampleModel model = reader.getRawImageType(inputIndex).getSampleModel(); bytesPerPixel = Math.max(1, model.getNumBands() * DataBuffer.getDataTypeSize(model.getDataType()) / Byte.SIZE); } final int initialTileCount = tiles.size(); /* * If the user do not wants to overwrite existing tiles (for faster processing when this * write process is started again after a previous failure), removes from the collection * every tiles which already exist. */ if (!policy.overwrite) { for (final Iterator<Tile> it=tiles.iterator(); it.hasNext();) { final Tile tile = it.next(); final Object input = tile.getInput(); if (input instanceof File) { final File file = (File) input; if (file.isFile()) { it.remove(); } } } } /* * Creates now the various other objects to be required in the loop. This include a * RTree initialized with the tiles remaining after the removal in the previous block. */ if (executor == null) { executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); } final List<Future<?>> tasks = new ArrayList<Future<?>>(); final TreeNode tree = new GridNode(tiles.toArray(new Tile[tiles.size()])); final ImageReadParam readParam = reader.getDefaultReadParam(); final Logger logger = Logging.getLogger(MosaicImageWriter.class); final boolean logWrites = logger.isLoggable(level); final boolean logReads = !(reader instanceof MosaicImageReader); if (!logReads) { ((MosaicImageReader) reader).setLogLevel(level); } if (writeParam != null) { if (writeParam.getSourceXSubsampling() != 1 || writeParam.getSubsamplingXOffset() != 0 || writeParam.getSourceYSubsampling() != 1 || writeParam.getSubsamplingYOffset() != 0 || writeParam.getSourceRegion() != null) { // TODO: Not yet supported. May be supported in a future version // if we have time to implement such support. throw new IllegalArgumentException(Errors.format( ErrorKeys.UNEXPECTED_ARGUMENT_FOR_INSTRUCTION_$1, "writeFromInput")); } readParam.setSourceBands(writeParam.getSourceBands()); } final long maximumMemory = getMaximumMemoryAllocation(); int maximumPixelCount = (int) (maximumMemory / bytesPerPixel); BufferedImage image = null; while (!tiles.isEmpty()) { if (abortRequested()) { processWriteAborted(); return false; } processImageProgress((initialTileCount - tiles.size()) * 100f / initialTileCount); /* * Gets the source region for some initial tile from the list. We will write as many * tiles as we can using a single image. The tiles successfully written will be removed * from the list, so next iterations will process only the remaining tiles. */ final Dimension imageSubsampling = new Dimension(); // Computed by next line. final Tile imageTile = getEnclosingTile(tiles, tree, imageSubsampling, (image != null) ? new Dimension(image.getWidth(), image.getHeight()) : null, maximumPixelCount); final Rectangle imageRegion = imageTile.getAbsoluteRegion(); awaitTermination(tasks); // Must be invoked before we touch to the image. if (image != null) { final int width = imageRegion.width / imageSubsampling.width; final int height = imageRegion.height / imageSubsampling.height; if (width == image.getWidth() && height == image.getHeight()) { ImageUtilities.fill(image, FILL_VALUE); assert isEmpty(image, new Rectangle(image.getWidth(), image.getHeight())); } else { image = null; } /* * Next iteration may try to allocate bigger images. We do that inconditionnaly, * even if current image fit, because current image may be small due to memory * constaint during a previous iteration. */ maximumPixelCount = (int) (maximumMemory / bytesPerPixel); } readParam.setDestination(image); readParam.setSourceRegion(imageRegion); readParam.setSourceSubsampling(imageSubsampling.width, imageSubsampling.height, 0, 0); if (readParam instanceof MosaicImageReadParam) { ((MosaicImageReadParam) readParam).setNullForEmptyImage(true); } if (logReads) { final LogRecord record = getLogRecord(false, VocabularyKeys.LOADING_$1, imageTile); record.setLoggerName(logger.getName()); logger.log(record); } /* * Before to attempt image loading, ask explicitly for a garbage collection cycle. * In theory we should not do that, but experience suggests that it really prevent * OutOfMemoryError when creating large images. If we still get OutOfMemoryError, * we will try again with smaller value of 'maximumPixelCount'. */ System.gc(); /* * Now process to the image loading. If we fails with an OutOfMemoryError (which * typically happen while creating the large BufferedImage), reduces the amount * of memory that we are allowed to use and try again. */ try { image = reader.read(inputIndex, readParam); } catch (OutOfMemoryError error) { maximumPixelCount >>>= 1; if (maximumPixelCount == 0) { throw error; } if (logWrites) { final LogRecord record = getLogRecord(true, LoggingKeys.RECOVERABLE_OUT_OF_MEMORY_$1, ((float) imageRegion.width * imageRegion.height) / (1024 * 1024f)); record.setLoggerName(logger.getName()); logger.log(record); } continue; } assert (image == null) || // Image can be null if MosaicImageReader found no tiles. (image.getWidth() * imageSubsampling.width == imageRegion.width && image.getHeight() * imageSubsampling.height == imageRegion.height) : imageTile; /* * Searchs tiles inside the same region with a resolution which is equals or lower by * an integer ratio. If such tiles are found we can write them using the image loaded * above instead of loading subimages. Giving that loading even a portion from a big * file may be long, the performance enhancement of doing so is significant. */ assert tiles.contains(imageTile) : imageTile; for (final Iterator<Tile> it=tiles.iterator(); it.hasNext();) { final Tile tile = it.next(); final Dimension subsampling = tile.getSubsampling(); if (!isDivisor(subsampling, imageSubsampling)) { continue; } final Rectangle sourceRegion = tile.getAbsoluteRegion(); if (!imageRegion.contains(sourceRegion)) { continue; } final int xSubsampling = subsampling.width / imageSubsampling.width; final int ySubsampling = subsampling.height / imageSubsampling.height; sourceRegion.translate(-imageRegion.x, -imageRegion.y); sourceRegion.x /= imageSubsampling.width; sourceRegion.y /= imageSubsampling.height; sourceRegion.width /= imageSubsampling.width; sourceRegion.height /= imageSubsampling.height; if (image != null && (policy.includeEmpty || !isEmpty(image, sourceRegion))) { final ImageWriter writer = getImageWriter(tile, image); final ImageWriteParam wp = writer.getDefaultWriteParam(); onTileWrite(tile, wp); wp.setSourceRegion(sourceRegion); wp.setSourceSubsampling(xSubsampling, ySubsampling, 0, 0); final IIOImage iioImage = new IIOImage(image, null, null); final Object tileInput = tile.getInput(); tasks.add(executor.submit(new Callable<Object>() { public Object call() throws IOException { if (!abortRequested()) { if (logWrites) { final LogRecord record = getLogRecord(false, VocabularyKeys.SAVING_$1, tile); record.setLoggerName(logger.getName()); logger.log(record); } boolean success = false; try { writer.write(null, iioImage, wp); close(writer.getOutput(), tileInput); success = true; } finally { if (success) { writer.dispose(); } else { final Object output = writer.getOutput(); writer.dispose(); close(output, null); // Unconditional close. if (tileInput instanceof File) { ((File) tileInput).delete(); } } } } return null; } })); } it.remove(); if (!tree.remove(tile)) { throw new AssertionError(tile); // Should never happen. } } assert !tiles.contains(imageTile) : imageTile; } awaitTermination(tasks); if (abortRequested()) { processWriteAborted(); return false; } else { processImageComplete(); return true; } } /** * Closes the given stream if it is different than the user object. When different, the * output is not the {@link File} (or whatever object) given by {@link Tile}. It is probably * an {@link ImageOutputStream} created by {@link #getImageWriter}, so we need to close it. */ private static void close(final Object stream, final Object user) throws IOException { if (stream != user) { if (stream instanceof Closeable) { ((Closeable) stream).close(); } else if (stream instanceof ImageInputStream) { ((ImageInputStream) stream).close(); } // Note: ImageOutputStream extends ImageInputStream, so the above check is suffisient. } } /** * Waits for every tasks to be completed. The tasks collection is emptied by this method. * * @throws IOException if at least one task threw a {@code IOException}. */ private final void awaitTermination(final List<Future<?>> tasks) throws IOException { Throwable exception = null; for (int i=0; i<tasks.size(); i++) { Future<?> task = tasks.get(i); try { task.get(); continue; } catch (ExecutionException e) { if (exception == null) { exception = e.getCause(); } // Abort after the catch block. } catch (InterruptedException e) { // Abort after the catch block. } abort(); for (int j=tasks.size(); --j>i;) { task = tasks.get(j); if (task.cancel(false)) { tasks.remove(j); } } } tasks.clear(); if (exception != null) { if (exception instanceof IOException) { throw (IOException) exception; } if (exception instanceof RuntimeException) { throw (RuntimeException) exception; } if (exception instanceof Error) { throw (Error) exception; } throw new UndeclaredThrowableException(exception); } } /** * Returns a log message for the given tile. */ private LogRecord getLogRecord(final boolean log, final int key, final Object arg) { final IndexedResourceBundle bundle = log ? Loggings.getResources(locale) : Vocabulary.getResources(locale); final LogRecord record = bundle.getLogRecord(level, key, arg); record.setSourceClassName(MosaicImageWriter.class.getName()); record.setSourceMethodName("writeFromInput"); return record; } /** * Returns a tile which enclose other tiles at finer resolution. Some tile layouts have a few * big tiles with low resolution covering the same geographic area than many smaller tiles with * finer resolution. If such overlapping is found, then this method returns one of the big tiles * and sets {@code imageSubsampling} to some subsampling that may be finer than usual for the * returned tile. Reading the big tile with that subsampling allows {@code MosaicImageWriter} * to use the same {@link RenderedImage} for writing both the big tile and the finer ones. * Example: * * <blockquote> * +-----------------------+ +-----------+-----------+ * | | |Subsampling|Subsampling| * | Subsampling | | = (2,2) | = (2,2) | * | = (4,4) | +-----------+-----------+ * | | |Subsampling|Subsampling| * | | | = (2,2) | = (2,2) | * +-----------------------+ +-----------+-----------+ * </blockquote> * * Given the above, this method will returns the tile illustrated on the left side and set * {@code imageSubsampling} to (2,2). * <p> * The algorithm implemented in this method is far from optimal and surely doesn't return * the best tile in all case. It is a compromize attempting to reduce the amount of image * data to load without too much CPU cost. * * @param tiles The tiles to examine. This collection is not modified. * @param tree Same as {@code tiles}, but as a tree for faster searchs. * @param imageSubsampling Where to store the subsampling to use for reading the tile. * This is an output parameter only. */ private static Tile getEnclosingTile(final List<Tile> tiles, final TreeNode tree, final Dimension imageSubsampling, Dimension preferredSize, final int maximumPixelCount) throws IOException { if (preferredSize != null) { final int area = preferredSize.width * preferredSize.height; if (area < maximumPixelCount / 2) { // The image size was small, probably due to memory constraint in a previous // iteration. Allow this method to try more aggresive memory usage. preferredSize = null; } } final Set<Dimension> subsamplingDone = tiles.size() > 24 ? new HashSet<Dimension>() : null; boolean selectedHasPreferredSize = false; int selectedCount = 0; Tile selectedTile = null; Tile fallbackTile = null; // Used only if we failed to select a tile. long fallbackArea = Long.MAX_VALUE; assert tree.containsAll(tiles); search: for (final Tile tile : tiles) { /* * Gets the collection of tiles in the same area than the tile we are examinating. * We will retain the tile with the largest filtered collection. Filtering will be * performed only if there is some chance to get a larger collection than the most * sucessful one so far. */ final Rectangle region = tile.getAbsoluteRegion(); final Dimension subsampling = tile.getSubsampling(); if (subsamplingDone != null && !subsamplingDone.add(subsampling)) { /* * In order to speedup this method, examine only one tile for each subsampling * value. This is a totally arbitrary choice but work well for the most common * tile layouts (constant area & constant size). Without such reduction, the * execution time of this method is too long for large tile collections. For * smaller collections, we can afford a systematic examination of all tiles. */ continue; } // Reminder: Collection in next line will be modified, so it needs to be mutable. final Collection<Tile> enclosed = tree.containedIn(region); assert enclosed.contains(tile) : tile; if (enclosed.size() <= selectedCount) { assert selectedTile != null : selectedCount; continue; // Already a smaller collection - no need to do more in this iteration. } /* * Found a collection that may be larger than the most successful one so far. First, * gets the smallest subsampling. We will require tiles at the finest resolution to * be written in the first pass before to go up in the pyramid. If they were written * last, those small tiles would be read one-by-one, which defeat the purpose of this * method (to read a bunch of tiles at once). */ Dimension finestSubsampling = subsampling; int smallestPixelArea = subsampling.width * subsampling.height; for (final Tile subtile : enclosed) { final Dimension s = subtile.getSubsampling(); final int pixelArea = s.width * s.height; if (pixelArea < smallestPixelArea) { smallestPixelArea = pixelArea; finestSubsampling = s; } } long area = (long) region.width * (long) region.height; area /= smallestPixelArea; if (area > maximumPixelCount) { if (area < fallbackArea) { fallbackArea = area; fallbackTile = tile; } continue; } /* * Found a subsampling that may be finer than the tile subsampling. Now removes * every tiles which would consume too much memory if we try to read them using * that subsampling. If the tiles to be removed are the finest ones, search for * an other tile to write because we really want the finest resolution to be * written first (see previous comment). */ for (final Iterator<Tile> it=enclosed.iterator(); it.hasNext();) { final Tile subtile = it.next(); final Dimension s = subtile.getSubsampling(); if (!isDivisor(subsampling, s)) { it.remove(); if (s.equals(finestSubsampling)) { continue search; } continue; } final Rectangle subregion = subtile.getAbsoluteRegion(); area = (long) subregion.width * (long) subregion.height; area /= smallestPixelArea; if (area > maximumPixelCount) { it.remove(); if (s.equals(finestSubsampling)) { continue search; } } } /* * Retains the tile with the largest filtered collection of sub-tiles. * A special case is made for tile having the preferred size, in order * to recycle the existing BufferedImage. */ final int tileCount = enclosed.size(); final boolean isPreferredSize = (preferredSize != null) && region.width / finestSubsampling.width == preferredSize.width && region.height / finestSubsampling.height == preferredSize.height; if (selectedTile == null || tileCount > selectedCount || (isPreferredSize && !selectedHasPreferredSize)) { selectedTile = tile; selectedCount = tileCount; selectedHasPreferredSize = isPreferredSize; imageSubsampling.setSize(finestSubsampling); } } /* * The selected tile may still null if 'maximumPixelCount' is so small than even the * smallest tile doesn't fit. We will return the smallest tile anyway, maybe letting * a OutOfMemoryError to occurs in the caller if really the tile can't hole in the * available memory. We perform this try anyway because estimation of available memory * in Java is only approximative. */ if (selectedTile == null) { selectedTile = fallbackTile; imageSubsampling.setSize(fallbackTile.getSubsampling()); } return selectedTile; } /** * Returns {@code true} if the given denominator is a divisor of the given numerator * for both {@linkplain Dimension#width width} and {@linkplain Dimension#height height}. */ private static boolean isDivisor(final Dimension numerator, final Dimension denominator) { return (numerator.width % denominator.width ) == 0 && (numerator.height % denominator.height) == 0; } /** * Returns the maximal amount of memory that {@link #writeFromInput writeFromInput} is allowed * to use. The default implementation computes a value from the amount of memory available in * the current JVM. Subclasses can override this method for returning a different value. * <p> * The returned value will be considered on a <cite>best effort</cite> basis. There is no * garantee that no more memory than the returned value will be used. * * @return An estimation of the maximum amount of memory allowed for allocation, in bytes. */ public long getMaximumMemoryAllocation() { final Runtime runtime = Runtime.getRuntime(); runtime.gc(); final long usedMemory = runtime.totalMemory() - runtime.freeMemory(); return Math.min(128L*1024*1024, runtime.maxMemory() - 2*usedMemory); } /** * Invoked after {@code MosaicImageWriter} has created a reader and * {@linkplain ImageReader#setInput(Object) set the input}. Users can override this method * for performing additional configuration and may returns {@code false} if the given reader * is not suitable. The default implementation returns {@code true} in all case. * * @param reader The image reader created and configured by {@code MosaicImageWriter}. * @return {@code true} If the given reader is ready for use, or {@code false} if an other * reader should be fetched. * @throws IOException if an error occured while inspecting or configuring the reader. */ protected boolean filter(final ImageReader reader) throws IOException { return true; } /** * Invoked after {@code MosaicImageWriter} has created a writer and * {@linkplain ImageWriter#setOutput(Object) set the output}. Users can override this method * for performing additional configuration and may returns {@code false} if the given writer * is not suitable. The default implementation returns {@code true} in all case. * * @param writer The image writer created and configured by {@code MosaicImageWriter}. * @return {@code true} If the given writer is ready for use, or {@code false} if an other * writer should be fetched. * @throws IOException if an error occured while inspecting or configuring the writer. */ protected boolean filter(final ImageWriter writer) throws IOException { return true; } /** * Returns {@code true} if the given region in the given image contains only fill values. * * @param image The image to test. * @param region The region in the image to test. */ private boolean isEmpty(final BufferedImage image, final Rectangle region) { int[] data = null; final Raster raster = image.getRaster(); final int xmax = region.x + region.width; final int ymax = region.y + region.height; for (int y=region.y; y<ymax; y++) { for (int x=region.x; x<xmax; x++) { data = raster.getPixel(x, y, data); for (int i=data.length; --i>=0;) { if (data[i] != FILL_VALUE) { return false; } } } } return true; } /** * Invoked automatically when a tile is about to be written. The default implementation does * nothing. Subclasses can override this method in order to set custom write parameters. * <p> * The {@linkplain ImageWriteParam#setSourceRegion source region} and * {@linkplain ImageWriteParam#setSourceSubsampling source subsampling} parameters can not be * set through this method. Their setting will be overwritten by the caller because their * values depend on the strategy choosen by {@code MosaicImageWriter} for reading images, * which itself depends on the amount of available memory. * * @param tile The tile to be written. * @param parameters The parameters to be given to the {@linkplain ImageWriter image writer}. * This method is allowed to change the parameter values. * @throws IOException if an I/O operation was required and failed. */ protected void onTileWrite(final Tile tile, ImageWriteParam parameters) throws IOException { } /** * Gets and initializes an {@linkplain ImageReader image reader} that can decode the specified * input. The returned reader has its {@linkplain ImageReader#setInput input} already set. If * the reader input is different than the specified one, then it is probably an {@linkplain * ImageInputStream image input stream} and closing it is caller's responsability. * * @param input The input to read. * @return The image reader that seems to be the most appropriated (never {@code null}). * @throws IOException If no suitable image reader has been found or if an error occured * while creating an image reader or initiazing it. */ private ImageReader getImageReader(final Object input) throws IOException { /* * First check if the input type is one of those accepted by MosaicImageReader. We * perform this check in a dedicaced code since MosaicImageReader is not registered * as a SPI, because it does not comply to Image I/O contract which requires that * readers accept ImageInputStream. */ for (final Class<?> type : MosaicImageReader.Spi.INPUT_TYPES) { if (type.isInstance(input)) { final ImageReader reader = new MosaicImageReader(); reader.setInput(input); if (filter(reader)) { return reader; } break; } } ImageInputStream stream = null; boolean createStream = false; /* * The following loop will be executed at most twice. The first iteration tries the given * input directly. The second iteration tries the input wrapped in an ImageInputStream. */ do { final Object candidate; if (createStream) { stream = ImageIO.createImageInputStream(input); if (stream == null) { continue; } candidate = stream; } else { candidate = input; } final Iterator<ImageReader> readers = ImageIO.getImageReaders(candidate); while (readers.hasNext()) { final ImageReader reader = readers.next(); reader.setInput(candidate); // If there is any more advanced check to perform, we should do it here. // For now we accept the reader inconditionnaly. if (filter(reader)) { return reader; } reader.dispose(); } } while ((createStream = !createStream) == true); if (stream != null) { stream.close(); } throw new IIOException(Errors.format(ErrorKeys.NO_IMAGE_READER)); } /** * Gets and initializes an {@linkplain ImageWriter image writer} that can encode the specified * image. The returned writer has its {@linkplain ImageWriter#setOutput output} already set. * If the output is different than the {@linkplain Tile#getInput tile input}, then it is * probably an {@linkplain ImageOutputStream image output stream} and closing it is caller's * responsability. * <p> * This method must returns a new instance. We are not allowed to cache and recycle writers, * because more than one writer may be used simultaneously. * * @param tile The tile to encode. * @param image The image associated to the specified tile. * @return The image writer that seems to be the most appropriated (never {@code null}). * @throws IOException If no suitable image writer has been found or if an error occured * while creating an image writer or initiazing it. */ private ImageWriter getImageWriter(final Tile tile, final RenderedImage image) throws IOException { // Note: we rename "Tile.input" as "output" because we want to write in it. final Object output = tile.getInput(); final Class<?> outputType = output.getClass(); final ImageReaderSpi readerSpi = tile.getImageReaderSpi(); final String[] formatNames = readerSpi.getFormatNames(); final String[] spiNames = readerSpi.getImageWriterSpiNames(); ImageOutputStream stream = null; // Created only if needed. /* * The search will be performed at most twice. In the first try (code below) we check the * plugins specified in 'spiNames' since we assume that they will encode the image in the * best suited format for the reader. In the second try (to be run if the first one found * no suitable writer), we look for providers by their name. */ if (spiNames != null) { final IIORegistry registry = IIORegistry.getDefaultInstance(); final ImageWriterSpi[] providers = new ImageWriterSpi[spiNames.length]; int count = 0; for (final String name : spiNames) { final Class<?> spiType; try { spiType = Class.forName(name); } catch (ClassNotFoundException e) { // May be normal. continue; } /* * For each image writers, checks if at least one format name is an expected one * and check if the writer can encode the image. If a suitable writter is found * and is capable to encode the output, returns it immediately. Otherwise the * writers are stored in an array as we found it, in order to try them again with * an ImageOutputStream after this loop. */ final Object candidate = registry.getServiceProviderByClass(spiType); if (candidate instanceof ImageWriterSpi) { final ImageWriterSpi spi = (ImageWriterSpi) candidate; final String[] names = spi.getFormatNames(); if (XArray.intersects(formatNames, names) && spi.canEncodeImage(image)) { providers[count++] = spi; for (final Class<?> legalType : spi.getOutputTypes()) { if (legalType.isAssignableFrom(outputType)) { final ImageWriter writer = spi.createWriterInstance(); writer.setOutput(output); if (filter(writer)) { return writer; } writer.dispose(); break; } } } } } /* * No provider accepts the output directly. This output is typically a File or URL. * Creates an image output stream from it and try again. */ if (count != 0) { stream = ImageIOExt.createImageOutputStream(image, output); if (stream != null) { final Class<? extends ImageOutputStream> streamType = stream.getClass(); for (int i=0; i<count; i++) { final ImageWriterSpi spi = providers[i]; for (final Class<?> legalType : spi.getOutputTypes()) { if (legalType.isAssignableFrom(streamType)) { final ImageWriter writer = spi.createWriterInstance(); writer.setOutput(stream); if (filter(writer)) { return writer; } writer.dispose(); break; } } } } } } /* * No suitable writer found from 'spiNames'. Try again using format name. * At the difference of the previous try, this one works on ImageWriters * instead of ImageWriterSpi instances. */ for (final String name : formatNames) { final List<ImageWriter> writers = new ArrayList<ImageWriter>(); final Iterator<ImageWriter> it = ImageIO.getImageWritersByFormatName(name); while (it.hasNext()) { final ImageWriter writer = it.next(); final ImageWriterSpi spi = writer.getOriginatingProvider(); if (spi == null || !spi.canEncodeImage(image)) { writer.dispose(); continue; } writers.add(writer); for (final Class<?> legalType : spi.getOutputTypes()) { if (legalType.isAssignableFrom(outputType)) { writer.setOutput(output); if (filter(writer)) { return writer; } // Do not dispose the writer since we will try it again later. break; } } } if (!writers.isEmpty()) { if (stream == null) { stream = ImageIOExt.createImageOutputStream(image, output); if (stream == null) { break; } } final Class<? extends ImageOutputStream> streamType = stream.getClass(); for (final ImageWriter writer : writers) { final ImageWriterSpi spi = writer.getOriginatingProvider(); for (final Class<?> legalType : spi.getOutputTypes()) { if (legalType.isAssignableFrom(streamType)) { writer.setOutput(stream); if (filter(writer)) { return writer; } break; } } writer.dispose(); } } } if (stream != null) { stream.close(); } throw new IIOException(Errors.format(ErrorKeys.NO_IMAGE_WRITER)); } /** * Returns the default stream metadata, or {@code null} if none. * The default implementation returns {@code null} in all cases. */ public IIOMetadata getDefaultStreamMetadata(ImageWriteParam param) { return null; } /** * Returns the default image metadata, or {@code null} if none. * The default implementation returns {@code null} in all cases. */ public IIOMetadata getDefaultImageMetadata(ImageTypeSpecifier imageType, ImageWriteParam param) { return null; } /** * Returns stream metadata initialized to the specified state, or {@code null}. * The default implementation returns {@code null} in all cases since this plugin * doesn't provide metadata encoding capabilities. */ public IIOMetadata convertStreamMetadata(IIOMetadata inData, ImageWriteParam param) { return null; } /** * Returns image metadata initialized to the specified state, or {@code null}. * The default implementation returns {@code null} in all cases since this plugin * doesn't provide metadata encoding capabilities. */ public IIOMetadata convertImageMetadata(IIOMetadata inData, ImageTypeSpecifier imageType, ImageWriteParam param) { return null; } /** * Disposes resources held by this writter. This method should be invoked when this * writer is no longer in use, in order to release some threads created by the writer. */ @Override public void dispose() { if (executor != null) { executor.shutdown(); executor = null; } super.dispose(); } // It is not strictly necessary to override finalize() since ThreadPoolExecutor // already invokes shutdown() in its own finalize() method. /** * Service provider for {@link MosaicImageWriter}. * * @since 2.5 * @source $URL$ * @version $Id$ * @author Cédric Briançon * @author Martin Desruisseaux */ public static class Spi extends ImageWriterSpi { /** * The default instance. */ public static final Spi DEFAULT = new Spi(); /** * Creates a default provider. */ public Spi() { vendorName = "GeoTools"; version = GeoTools.getVersion().toString(); names = MosaicImageReader.Spi.NAMES; outputTypes = MosaicImageReader.Spi.INPUT_TYPES; pluginClassName = "org.geotools.image.io.mosaic.MosaicImageWriter"; } /** * Returns {@code true} if this writer is likely to be able to encode images with the given * layout. The default implementation returns {@code true} in all cases. The capability to * encode images depends on the tile format specified in {@link Tile} objects, which are * not known to this provider. */ public boolean canEncodeImage(ImageTypeSpecifier type) { return true; } /** * Returns a new {@link MosaicImageWriter}. * * @throws IOException If an I/O operation was required and failed. */ public ImageWriter createWriterInstance(Object extension) throws IOException { return new MosaicImageWriter(this); } /** * Returns a brief, human-readable description of this service provider. * * @todo Localize. */ public String getDescription(final Locale locale) { return "Mosaic Image Writer"; } } }