/* * Geotoolkit.org - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2008-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.mosaic; import java.awt.Point; 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.awt.image.BufferedImageOp; import java.awt.geom.AffineTransform; import java.io.File; import java.io.IOException; import java.io.InterruptedIOException; import javax.imageio.*; // Lot of them in this class. import javax.imageio.spi.IIORegistry; import javax.imageio.spi.ImageReaderSpi; import javax.imageio.spi.ImageReaderWriterSpi; import javax.imageio.spi.ImageWriterSpi; import javax.imageio.metadata.IIOMetadata; import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.ImageOutputStream; import java.nio.file.FileStore; import java.nio.file.Files; import java.nio.file.Path; import java.util.*; // Lot of them in this class. import java.util.logging.Level; import java.util.logging.LogRecord; import java.util.concurrent.Future; import java.util.concurrent.Callable; import java.util.concurrent.Semaphore; import java.util.concurrent.Executors; import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutionException; import java.util.concurrent.ThreadPoolExecutor; import java.lang.reflect.UndeclaredThrowableException; import org.apache.sis.math.MathFunctions; import org.apache.sis.util.ArraysExt; import org.geotoolkit.coverage.io.CoverageIO; import org.geotoolkit.util.Utilities; import org.apache.sis.util.Disposable; import org.apache.sis.util.logging.Logging; import org.geotoolkit.util.logging.LogProducer; import org.apache.sis.util.logging.PerformanceLevel; import org.geotoolkit.resources.Errors; import org.geotoolkit.resources.Loggings; import org.geotoolkit.resources.Vocabulary; import org.apache.sis.util.resources.IndexedResourceBundle; import org.geotoolkit.image.internal.ImageUtilities; import org.geotoolkit.image.io.InvalidImageStoreException; import org.geotoolkit.image.io.UnsupportedImageFormatException; import org.geotoolkit.internal.Threads; import org.geotoolkit.internal.image.io.SupportFiles; import org.geotoolkit.internal.image.io.RawFile; import org.geotoolkit.nio.IOUtilities; import org.geotoolkit.internal.io.TemporaryFile; import static org.geotoolkit.image.io.mosaic.Tile.LOGGER; /** * An image writer which takes a large image (potentially tiled) in input and write tiles as * output. The mosaic to write is specified as a collection of {@link Tile} objects given to * the {@link #setOutput(Object)} method. The pixel values to write can be specified either * as a {@link RenderedImage} (this is the {@linkplain #write standard API}), or as a single * {@link File} or a collection of source tiles given to the {@link #writeFromInput(Object, * ImageWriteParam)} method. The later alternative is non-standard but often required since * the image to mosaic is typically bigger than the capacity of a single {@link RenderedImage}. * * {@section Caching of source tiles} * This class may be slow when reading source images encoded in a compressed format like PNG, * because multiple passes over the same image may be necessary for writing different tiles * and compression makes the seeks harder. This problem can be mitigated by copying the source * images to temporary files in an uncompressed RAW format. The inconvenient is that a large * amount of disk space will be temporarily required until the write operation is completed. * <p> * Caching are enabled by default. If the environment is constrained by disk space or if the * source tiles are known to be already uncompressed, then caching can be disabled by overriding * the {@link #isCachingEnabled(ImageReader,int)} method. * * {@section Filtering source images} * It is possible to apply an operation on source images before to create the target tiles. The * operation can be specified by the {@link MosaicImageWriteParam#setSourceTileFilter(BufferedImageOp)} * method, for example in order to add transparency to fully opaque images. Note that if an operation * is applied, then the source tiles will be cached in temporary RAW files as described in the above * section even if {@link #isCachingEnabled(ImageReader,int)} returns {@code false}. * * @author Martin Desruisseaux (Geomatys) * @author Cédric Briançon (Geomatys) * @version 3.18 * * @since 2.5 * @module */ public class MosaicImageWriter extends ImageWriter implements LogProducer, Disposable { /** * 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 preferred tile size inside the images. This apply only to format that support tile * size, like TIFF. The tile size effectively used may be slightly different. * * {@note The default value used by GDAL is 64.} * * @since 3.15 */ private static final int IMAGE_TILE_SIZE = 64; /** * The logging level for tiling information during read and write operations. If {@code null}, * then the level shall be selected by {@link PerformanceLevel#forDuration(long, TimeUnit)}. */ private Level logLevel; /** * The temporary files created for each input tile. */ private final Map<Tile,RawFile> temporaryFiles; /** * 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); temporaryFiles = new HashMap<>(); } /** * Returns {@code true} if logging is enabled. */ private boolean isLoggable() { Level level = logLevel; if (level == null) { level = PerformanceLevel.SLOWEST; } return LOGGER.isLoggable(level); } /** * Returns the logging level is explicitely set, or the {@link Level#FINE} level * otherwise. This is used for logging operations that are not measurement of * execution time. */ private Level getFineLevel() { final Level level = logLevel; return (level != null) ? level : PerformanceLevel.FINE; } /** * Returns the logging level for tile information during read and write operations. * The default value is one of the {@link PerformanceLevel} constants, determined * according the duration of the operation. * * @return The current logging level. */ @Override public Level getLogLevel() { final Level level = logLevel; return (level != null) ? level : PerformanceLevel.PERFORMANCE; } /** * Sets the logging level for tile information during read and write operations. * A {@code null} value restores the default level documented in the {@link #getLogLevel()} * method. * * @param level The new logging level, or {@code null} for the default. */ @Override public void setLogLevel(Level level) { logLevel = 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 very 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 occurred while writing the image. */ @Override 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' method 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 { try (ImageOutputStream output = ImageIO.createImageOutputStream(file)) { writer.setOutput(output); writer.write(metadata, image, param); } /* * 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 UnsupportedImageFormatException(Errors.format(Errors.Keys.NoImageWriter)); } /** * Reads the image from the given input and writes it as a set of tiles. This is equivalent * to <code>{@linkplain #writeFromInput(Object,int,ImageWriteParam) writeFromInput}(input, * <b>0</b>, param)</code> except that this method ensures that the input contains only one * image. If more than one image is found, then an exception is throw. This is often desirable * when the input is a collection of {@link Tile}s, since having more than one "image" (where * "image" in this context means an input mosaic as a whole) means that we failed to create a * single mosaic from a set of source tiles. * * @param input The image input, typically as a {@link 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 occurred while reading or writing. * * @since 3.00 */ public boolean writeFromInput(final Object input, final ImageWriteParam param) throws IOException { return writeFromInput(input, 0, param, true); } /** * 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 occurred 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 { return writeFromInput(input, inputIndex, param, false); } /** * Implements the public {@code writeFromInput} method. * * @param onlyOneImage If {@code true}, then the operation fails if the input contains more than * one image. This is often necessary if the input is a collection of {@link TileManager}s, * since more than 1 image means that the manager failed to create a single mosaic from * a set of source images. */ final boolean writeFromInput(final Object input, final int inputIndex, final ImageWriteParam param, boolean onlyOneImage) throws IOException { final boolean success; final ImageReader reader = getImageReader(input, inputIndex, param); final Queue<ReaderInputPair.WithWriter> cache = new LinkedList<>(); try { if (onlyOneImage && reader.getNumImages(false) <= 1) { onlyOneImage = false; } // Write the image only if 'onlyOneImage' is false, otherwise we already failed. success = !onlyOneImage && writeFromReader(reader, inputIndex, param, cache); close(reader.getInput(), input); } finally { /* * Make sure that we delete the temporary files. This is the most important * cleanup. Other cleanup (disposing the ImageWriters) is good practice but * less important since the garbage collector would collect them anyway. */ try { reader.dispose(); } finally { deleteTemporaryFiles(); } synchronized (cache) { for (final ReaderInputPair.WithWriter entry : cache) { entry.writer.dispose(); } cache.clear(); } } if (onlyOneImage) { throw new InvalidImageStoreException(Errors.getResources(locale) .getString(Errors.Keys.IllegalMosaicInput)); } return success; } /** * Reads the image from the given reader and writes it as a set of tiles. * It is the caller responsibility to dispose the reader when writing is done. * * @param reader The image reader configured for reading the image to mosaic. This reader * should have been created by {@link #getImageReader(Object, int, ImageWriteParam)}. */ private boolean writeFromReader(final ImageReader reader, final int inputIndex, final ImageWriteParam writeParam, final Queue<ReaderInputPair.WithWriter> cache) 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.DEFAULT; } processImageStarted(outputIndex); /* * Gets the reader first - especially before getOutput() - because the user may have * overridden filter(ImageReader) and set the output accordingly. TileBuilder does that. */ final TileManager[] managers = getOutput(); if (managers == null) { throw new IllegalStateException(Errors.format(Errors.Keys.NoImageOutput)); } final List<Tile> tiles; final int bytesPerPixel; if (policy == TileWritingPolicy.NO_WRITE) { tiles = Collections.emptyList(); bytesPerPixel = 1; } else { tiles = new LinkedList<>(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; final ImageTypeSpecifier type = reader.getRawImageType(inputIndex); if (type != null && (model = type.getSampleModel()) != null) { bytesPerPixel = Math.max(1, model.getNumBands() * DataBuffer.getDataTypeSize(model.getDataType()) / Byte.SIZE); } else { bytesPerPixel = 3; // Assuming RGB, since we don't have better information. } } final int initialTileCount = tiles.size(); final float progressScale = 100f / initialTileCount; /* * 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. */ final int nThreads = Runtime.getRuntime().availableProcessors(); final ExecutorService executor = Executors.newFixedThreadPool(nThreads, Threads.createThreadFactory("MosaicImageWriter #")); final Semaphore submitPermits = new Semaphore(nThreads + 1); final List<Future<?>> tasks = new ArrayList<>(); final TreeNode tree = new GridNode(tiles.toArray(new Tile[tiles.size()])); final ImageReadParam readParam = reader.getDefaultReadParam(); final boolean logWrites = isLoggable(); final boolean logReads = !(reader instanceof LogProducer); if (!logReads) { // Use the field, not the local variable, because we want null for the default logging. ((LogProducer) reader).setLogLevel(logLevel); } 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( Errors.Keys.UnexpectedArgumentForInstruction_1, "writeFromInput")); } readParam.setSourceBands(writeParam.getSourceBands()); } final long maximumMemory = getMaximumMemoryAllocation(); int maximumPixelCount = (int) (maximumMemory / bytesPerPixel); BufferedImage image = null; int cachedImageWidth = 0, cachedImageTileWidth = 0, cachedImageHeight = 0, cachedImageTileHeight = 0; while (!tiles.isEmpty()) { if (abortRequested()) { processWriteAborted(); executor.shutdown(); return false; } /* * 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, maximumPixelCount); final Rectangle imageRegion = imageTile.getAbsoluteRegion(); /* * Following line must be invoked before we touch to the image. It makes sure that * all background threads have finished their work with the previous image content. */ awaitTermination(tasks, initialTileCount - tiles.size(), progressScale); 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(width, height)); } else { // Needs a bigger image than what we had previously. image = null; } /* * Next iteration may try to allocate bigger images. We do that unconditionally, * even if current image fits, because current image may be small due to memory * constraint 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) { log(false, Vocabulary.Keys.Loading_1, imageTile); } /* * 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 prevents * 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 fail with an OutOfMemoryError (which * typically happen while creating the large BufferedImage), reduce 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) { log(true, Loggings.Keys.RecoverableOutOfMemory_1, ((float) imageRegion.width * imageRegion.height) / (1024 * 1024f)); } // Go back to the while(!tiles.isEmpty()) condition, which will ask for a // new Tile from the same (not yet modified) collection of tiles but with // a smaller maximumPixelCount value. continue; } if (logWrites && image != readParam.getDestination()) { // Logs only if we have created a new image. logMemoryUsage(); } 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; /* * Searches 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))) { /* * Before to create a new ImageWriter, wait for the executor to catch up * with pending tasks. If we enqueue every tasks without waiting, we would * have a lot of ImageWriter instances with only a few of them actually in * use. */ try { submitPermits.acquire(); } catch (InterruptedException e) { throw new InterruptedIOException(e.getLocalizedMessage()); } /* * Get an image writer (maybe from the cache) and configure it. We do the * configuration here because we want the 'filter' and 'onTileWrite' methods * (which may be overridden by the user) to be invoked in this thread. */ final ReaderInputPair.WithWriter cacheEntry = getImageWriter(tile, image, cache); final ImageWriteParam wp = cacheEntry.writer.getDefaultWriteParam(); onTileWrite(tile, wp); wp.setSourceRegion(sourceRegion); wp.setSourceSubsampling(xSubsampling, ySubsampling, 0, 0); if (wp.canWriteTiles()) { if (cachedImageWidth != sourceRegion.width) { cachedImageWidth = sourceRegion.width; cachedImageTileWidth = imageTileSize(cachedImageWidth); } if (cachedImageHeight != sourceRegion.height) { cachedImageHeight = sourceRegion.height; cachedImageTileHeight = imageTileSize(cachedImageHeight); } wp.setTilingMode(ImageWriteParam.MODE_EXPLICIT); wp.setTiling(cachedImageTileWidth, cachedImageTileHeight, 0, 0); } final IIOImage iioImage = new IIOImage(image, null, null); /* * Submit the image write for execution in a background thread. */ tasks.add(executor.submit(new Callable<Object>() { @Override public Object call() throws IOException { final Object tileInput; try { if (abortRequested()) { return null; } if (logWrites) { log(false, Vocabulary.Keys.Saving_1, tile); } tileInput = tile.getInput(); final ImageWriter writer = cacheEntry.writer; boolean success = false; try { writer.write(null, iioImage, wp); close(writer.getOutput(), tileInput); success = true; } finally { if (success) { /* * The write operation succeed: return the ImageWriter * to the pool of writers, so the next write operation * can recycle it. */ writer.reset(); synchronized (cache) { cache.add(cacheEntry); } } else { /* * The write operation failed. Dispose the ImageWriter * (do not return it to the pool) and delete the file * if possible. The exception that caused the failure * will be propagated after this block. */ final Object output = writer.getOutput(); writer.dispose(); close(output, null); // Unconditional close. if (tileInput instanceof File) { ((File) tileInput).delete(); } } } } finally { submitPermits.release(); } /* * Write the TFW file. */ if (tileInput instanceof File) { AffineTransform gridToCRS = tile.getGridToCRS(); if (gridToCRS != null) { gridToCRS = new AffineTransform(gridToCRS); final Point location = tile.getLocation(); gridToCRS.translate(location.x, location.y); SupportFiles.writeTFW((File) tileInput, gridToCRS); } } return null; } })); /* * While submitPermits.acquire() was waiting in the above code, some image * writers may have been pushed back to the cache. Ensure that the cache is * keept to a reasonable size by disposing, if needed, the oldest writers. * We do this cleanup here instead than after submitPermits.acquire() because * getImageWriter(...) may have selected a writer that we would otherwise have * disposed. */ synchronized (cache) { while (cache.size() > 2*nThreads) { cache.remove().writer.dispose(); } if (false) { // Removed from compilation except when debugging. final ThreadPoolExecutor e = (ThreadPoolExecutor) executor; LOGGER.log(getFineLevel(), "Active threads: {0} " + "Enqueued tasks: {1} " + "Available permits: {2} " + "Available ImageWriters: {3}", new Integer[] { e.getActiveCount(), e.getQueue().size(), submitPermits.availablePermits(), cache.size() }); } } } /* * The current tile has been processed. It may have been discarded because it * contains only transparent pixels, or it may have been enqueued for writing * in a background thread. Remove this tile from the list of tiles to process, * and inspect the next tile. */ it.remove(); if (!tree.remove(tile)) { throw new AssertionError(tile); // Should never happen. } } assert !tiles.contains(imageTile) : imageTile; } /* * At this point, every tiles have been submitted for writing. Wait for the write * operations to complete. The remaining ImageWriter instances will be disposed by * the caller. */ awaitTermination(tasks, initialTileCount, progressScale); executor.shutdown(); 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) { IOUtilities.close(stream); } } /** * Waits for every tasks to be completed. The tasks collection is emptied by this method. * * @param tasks The list of tasks to wait for completion. * @param done The number of tiles written so far, assuming that every pending tasks are * already completed. * @param progressScale The factor by which to multiply the number of tiles done in order * to get a percentage of completion. * @throws IOException if at least one task threw a {@code IOException}. */ private void awaitTermination(final List<Future<?>> tasks, int done, final float progressScale) throws IOException { done -= tasks.size(); Throwable exception = null; for (int i=0; i<tasks.size(); i++) { Future<?> task = tasks.get(i); try { task.get(); processImageProgress(progressScale * done++); 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); } } /** * Logs message for the given tile. * * @param log {@code true} if the given key is from the {@link Loggings} bundle, * or {@code false} if it is from the {@link Vocabulary} bundle. */ private void log(final boolean log, final short key, final Object arg) { final IndexedResourceBundle bundle = log ? Loggings.getResources(locale) : Vocabulary.getResources(locale); final LogRecord record = bundle.getLogRecord(getFineLevel(), key, arg); record.setSourceClassName(MosaicImageWriter.class.getName()); record.setSourceMethodName("writeFromInput"); record.setLoggerName(LOGGER.getName()); LOGGER.log(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: * * {@preformat text * ┌───────────────────────┐ ┌───────────┬───────────┐ * │ │ │Subsampling│Subsampling│ * │ Subsampling │ │ = (2,2) │ = (2,2) │ * │ = (4,4) │ ├───────────┼───────────┤ * │ │ │Subsampling│Subsampling│ * │ │ │ = (2,2) │ = (2,2) │ * └───────────────────────┘ └───────────┴───────────┘ * } * * 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 searches. * @param imageSubsampling Where to store the subsampling to use for reading the tile. * This is an output parameter only. * @param maximumPixelCount Tries to return a tile having an area smaller than this limit. * This is only a hint honored on a "best effort" basis: there is no guarantees that * this limit will actually be respected. */ private static Tile getEnclosingTile(final List<Tile> tiles, final TreeNode tree, final Dimension imageSubsampling, final int maximumPixelCount) throws IOException { final Set<Dimension> subsamplingDone = tiles.size() > 24 ? new HashSet<Dimension>() : null; 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) { // Retains the smallest one of the too big tiles. // To be used only if no suitable tile is found. 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. * We count the number of tiles remaining after the removal performed * by the code above. */ final int tileCount = enclosed.size(); if (selectedTile == null || tileCount > selectedCount) { selectedTile = tile; selectedCount = tileCount; 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 fit 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 * guarantee 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(); final long maxMemory = runtime.maxMemory(); runtime.gc(); long memory = runtime.freeMemory(); /* * Add the amount of memory that the JVM is still allowed to allocate from the OS. */ memory += maxMemory - runtime.totalMemory(); /* * Keep a safety margin of 12.5% of the maximum available memory. This is 8 Mb if the * default maximum memory is 64 Mb. This is 128 Mb if the maximum memory is 1 Gb. */ memory -= maxMemory / 8; /* * Return at least 16 Mb, assuming that we can't do much with less than that. */ return Math.max(16L*1024*1024, memory); } /** * Send to the given logger some informations about current memory usage. */ private void logMemoryUsage() { final Vocabulary resources = Vocabulary.getResources(locale); final Runtime runtime = Runtime.getRuntime(); final long totalMemory = runtime.totalMemory(); final double usage = 1 - (float) runtime.freeMemory() / totalMemory; final LogRecord record = new LogRecord(getFineLevel(), resources.getString(Vocabulary.Keys.MemoryHeapSize_1, totalMemory / (1024*1024L)) + ". " + resources.getString(Vocabulary.Keys.MemoryHeapUsage_1, usage) + '.'); record.setSourceClassName(MosaicImageWriter.class.getName()); record.setSourceMethodName("writeFromInput"); // The public API invoking this method. record.setLoggerName(LOGGER.getName()); LOGGER.log(record); } /** * Returns {@code true} if this writer is allowed to copy source images to temporary * uncompressed RAW files. Doing so can speed up considerably the creation of large * mosaics, at the expense of temporary disk space. * <p> * The default implementation returns {@code true} if the following conditions are meet: * <p> * <ul> * <li>The input tiles format is not an uncompressed format like RAW, BMP or TIFF (the * later is assumed uncompressed, while this is not always true). There is no * advantage to copy an uncompressed format to an other uncompressed format.</li> * <li>The {@linkplain File#getUsableSpace() usable space} in the temporary directory * is greater then the space required for writing the input tiles in RAW format.</li> * <li>The usable space in the output directory is greater than the space required for * writing the output tiles in their output format (a very approximative compression * factor is guessed).</li> * <li>Other conditions (not documented because they may change in future implementations). * In case of doubt, it is safer to conservatively return {@code false}.</li> * </ul> * <p> * Subclasses should override this method if they can provide a better answer, or if they * known that their source tiles are already uncompressed. * * {@note This method is invoked only in the context of <code>writeFromInput(Object, …)</code> * methods, which get the image to write from an <code>ImageReader</code>. This method is * not invoked in the context of the <code>write(RenderedImage)</code> method because an * image available in memory (ignoring JAI tiling) is assumed to not need disk cache. For * this reason there is no <code>isCachingEnabled(RenderedImage)</code> method.} * * @param input The input image or mosaic, an an {@code ImageReader} with its * {@linkplain ImageReader#getInput() input} set. * @param inputIndex The image index to read from the given input file. * @return {@code true} if this writer can cache the source tiles. * @throws IOException If this method required an I/O operation and that operation failed. * * @since 3.00 */ protected boolean isCachingEnabled(final ImageReader input, final int inputIndex) throws IOException { /* * Gets an estimation of the available memory. This will be used for computing the maximal * size of input images. The calculation will need to take in account the number of bits per * pixels. Note that we use "bits", not "bytes". We do not divide bitPerPixels by Byte.SIZE * now, because we don't want the rounding toward zero here. */ final Runtime rt = Runtime.getRuntime(); rt.gc(); final long maxInputSize = rt.maxMemory() - (rt.totalMemory() - rt.freeMemory()); final int bitPerPixels = bitsPerPixel(input.getRawImageType(inputIndex)); /* * Checks the space available in the temporary directory, which * will contain the temporary uncompressed files for source tiles. */ final Path sharedTemporaryDirectory = TemporaryFile.getSharedTemporaryDirectory(); final FileStore fileStore = Files.getFileStore(sharedTemporaryDirectory); long available = fileStore.getUsableSpace(); long required = 4L * 1024 * 1024; // Arbitrary margin of 4 Mb. if (input instanceof MosaicImageReader) { boolean compressed = false; for (final TileManager tiles : ((MosaicImageReader) input).getInput()) { /* * If there is not enough disk space for holding the uncompressed files, * stops immediately the check: we can not cache the tiles. */ required += tiles.diskUsage() * bitPerPixels / Byte.SIZE; if (required > available) { return false; } /* * If there is not enough memory for holding fully the largest tile, * stops immediately the check: we can not cache the tiles. */ if (tiles.largestTileArea() * bitPerPixels / Byte.SIZE > maxInputSize) { return false; } /* * Checks if there is at least one uncompressed tile. As soon as we have * found one such uncompressed file, we will not need to check anymore. */ if (!compressed) { for (final ImageReaderSpi spi : tiles.getImageReaderSpis()) { if (guessCompressionRatio(spi) != 1) { compressed = true; break; } } } } /* * If every tiles are written in an uncompressed format like RAW, then there * is no advantage to copy them to an other uncompressed format. Note that we * consider TIFF as uncompressed, which is what we get by default using the * Java image I/O writers. */ if (!compressed) { return false; } } else { /* * Same tests than above, but in the case where the source is a single image * instead than a mosaic of source tiles. */ if (guessCompressionRatio(input.getOriginatingProvider()) == 1) { return false; } final int width = input.getWidth (inputIndex); final int height = input.getHeight(inputIndex); final long size = ((long) width) * ((long) height) * bitPerPixels / Byte.SIZE; if (size > maxInputSize || (required += size) > available) { return false; } } /* * Checks the space available for the target tiles, which may be compressed. * Note that we do not reset the 'required' count to zero - we assume that * the target directory is on the same device than the temporary directory, * so the required space needs to be summed. I'm not aware of an easy way to * check if we are on the same device in Java 6, however new Java 7 API would * allow to do so. */ final TileManager[] outputs = getOutput(); if (outputs != null) { for (final TileManager tiles : outputs) { final Path root = tiles.rootDirectory(); if (root == null) { continue; // Not a file - there is nothing we can do. } available = Files.getFileStore(root).getUsableSpace(); long usage = tiles.diskUsage() * bitPerPixels / Byte.SIZE; for (final ImageReaderSpi spi : tiles.getImageReaderSpis()) { int ir = guessCompressionRatio(spi); if (ir != 0) { usage /= ir; break; // Use the first known format as the reference. } } required += usage; if (required > available) { return false; } } } return true; } /** * 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 occurred 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 occurred 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 chosen 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 * {@linkplain BufferedImageOp#filter filtered} 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 responsibility. * * @param input The input to read. * @param inputIndex The image index to read from the given input file. * @param The parameters given by the user, or {@code null} if none. * @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 occurred * while creating an image reader or initializing it. */ private ImageReader getImageReader(final Object input, final int inputIndex, final ImageWriteParam parameters) throws IOException { BufferedImageOp op = null; final ImageReader reader = getImageReader(input); if (parameters instanceof MosaicImageWriteParam) { final MosaicImageWriteParam param = (MosaicImageWriteParam) parameters; if (param.getTileWritingPolicy() == TileWritingPolicy.NO_WRITE) { return reader; } op = param.getSourceTileFilter(); } /* * If we are allowed to cache an uncompressed copies of the tiles, do that now. */ if (op != null || isCachingEnabled(reader, inputIndex)) { final Collection<Tile> tiles = new ArrayList<>(); if (reader instanceof MosaicImageReader) { for (final TileManager manager : ((MosaicImageReader) reader).getInput()) { tiles.addAll(manager.getTiles()); } } else { // Note: we must not use reader.getInput() since it may be an ImageInputStream. tiles.add(new Tile(reader.getOriginatingProvider(), input, 0, new Point(), (Dimension) null)); } /* * TODO: Folllowing has been disabled for now because the whole org.geotoolkit.internal.rmi package * has been removed. This is because JDK8 provides a new "fork join" framework make our stuff * obsolete (but we didn't migrated to JDK8 yet). The plan is to rewrite the functionality * from scratch in the Apache SIS project. Since the temporary files are optional, disabling * this code should not break the application. It may make it much slower, but we had issues * with the RAW format used by this code anyway. */ // temporaryFiles.putAll(RMI.execute(new TileCopier(tiles, op))); } return reader; } /** * 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 responsibility. * <p> * This method partially duplicates the work of * {@link org.geotoolkit.image.io.XImageIO#getReader(Object, Boolean, Boolean)} except for the * special handling of mosaic input (which may use the internal {@link CachingMosaicReader}) * and the calls to the user-overrideable {@link #filter(ImageReader)} method. * * @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 occurred * while creating an image reader or initializing 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 because we replace the default reader * by our custom CachingMosaicReader. */ if (MosaicImageReader.Spi.DEFAULT.canDecodeInput(input)) { // Creates an instance of CachingMosaicReader unconditionally, even if // the map of temporary files is empty, because it may be filled later. final ImageReader reader = new CachingMosaicReader(temporaryFiles, input); if (filter(reader)) { return reader; } reader.dispose(); } 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) { try { stream = CoverageIO.createImageInputStream(input); } catch (IOException e) { //input not supported 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 unconditionally. if (filter(reader)) { return reader; } reader.dispose(); } } while ((createStream = !createStream) == true); if (stream != null) { stream.close(); } /* * Before to give up, if the input is a file or a directory, try to open it has a mosaic. */ Exception cause = null; if (input instanceof File || input instanceof Path) { Path path = (input instanceof File) ? ((File) input).toPath() : (Path) input; TileManager[] managers = null; try { managers = TileManagerFactory.DEFAULT.create(path); } catch (Exception e) { // Catch: IOException and RuntimeException cause = e; } if (managers != null) { final ImageReader reader = new CachingMosaicReader(temporaryFiles, managers); if (filter(reader)) { return reader; } reader.dispose(); } } throw new UnsupportedImageFormatException(Errors.format(Errors.Keys.NoImageReader), cause); } /** * Gets and initializes an {@link ImageWriter} 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 {@link ImageOutputStream} and closing it is caller responsibility. * <p> * This method extracts an {@code ImageWriter} instance from the given cache, if possible. * If no suitable writer is available, then a new one is created but <strong>not</strong> * cached; it is caller responsibility to reset the writer and cache it after the write * operation has been completed. * * @param tile The tile to encode. * @param image The image associated to the specified tile. * @param cache An initially empty list of image writers created during the write process. * @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 occurred * while creating an image writer or initializing it. */ private ReaderInputPair.WithWriter getImageWriter(final Tile tile, final RenderedImage image, final Queue<ReaderInputPair.WithWriter> cache) 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(); ImageOutputStream stream = null; // Created only if needed. /* * The result of this method is determined entirely by the (readerSpi, outputType) * pair and by implementation of the user-overrideable filter(ImageWriter) method. * We will search iteratively for the first suitable entry. * * Note: Using Map<ReaderInputPair, Queue<ImageWriter>> could be more performant than * the iteration performed below, but the queue is usually very short since its length * is approximatively the number of processors. In addition, in the typical case where * all tiles use the same format, the iterator will stop at the first item in the queue. * So a Map would really bring no performance benefit for the "normal" case at the cost * of more complex code (harder to count the total number of ImageWriters and to dispose * the oldest ones). */ final ReaderInputPair.WithWriter cacheEntry = new ReaderInputPair.WithWriter(readerSpi, outputType); if (cache != null) { ReaderInputPair.WithWriter candidate = null; synchronized (cache) { for (final Iterator<ReaderInputPair.WithWriter> it=cache.iterator(); it.hasNext();) { final ReaderInputPair.WithWriter c = it.next(); if (cacheEntry.equals(c)) { candidate = c; it.remove(); break; } } } /* * If we have found a candidate, define its output and check if filter(ImageWriter) * accepts it. If the image writer is not accepted, the remaining of this method will * try to get an other instance from the IIORegistry. * * Note that the remaining of this method basically perform the same check 4 times, * each time using a different way to get the ImageWriter instances to test. */ if (candidate != null) { final ImageWriter writer = candidate.writer; if (candidate.needStream) { stream = ImageIO.createImageOutputStream(output); writer.setOutput(stream); } else { writer.setOutput(output); } if (filter(writer)) { return candidate; } writer.dispose(); } } /* * The search will be performed using two different strategies: * * 1) Check the plugins specified in 'spiNames' since we assume that they * will encode the image in the best suited format for the reader. * 2) (to be run if the above strategy did not found a suitable writer), * look for providers by their format name. */ int spiNameIndex = 0; final String[] spiNames = readerSpi.getImageWriterSpiNames(); final String[] formatNames = readerSpi.getFormatNames(); final IIORegistry registry = IIORegistry.getDefaultInstance(); final Set<ImageWriterSpi> providers = new LinkedHashSet<>(); List<ImageWriterSpi> ignored = null; // To be initialized when first needed. Iterator<ImageWriterSpi> it = null; // To be initialized when first needed. boolean canIgnore = true; while (true) { // The exit point is in the middle of the loop, after "if (!it.hasNext())". final ImageWriterSpi spi; if (spiNames != null && spiNameIndex < spiNames.length) { /* * ---- First strategy (see above comment) -------- */ final String spiName = spiNames[spiNameIndex++]; final Class<?> spiType; try { spiType = Class.forName(spiName); } catch (ClassNotFoundException e) { // May be normal. Search for an other writer. Logging.recoverableException(LOGGER, MosaicImageWriter.class, "getImageWriter", e); continue; } final Object candidate = registry.getServiceProviderByClass(spiType); if (!(candidate instanceof ImageWriterSpi)) { // No instance is registered for the given class. Search an other writer. continue; } spi = (ImageWriterSpi) candidate; } else { /* * ---- Second strategy (see above comment) -------- * To be run only after every providers * listed in 'spiNames' have been tried. */ if (it == null) { // Executed the first time that the second strategy is run. it = registry.getServiceProviders(ImageWriterSpi.class, true); } /* * If we have examined every pertinent providers listed by IIORegistry but some * of those providers have been ignored, and if we found no "normal" provider, * then take a second look to the providers that we have ignored. * * The ignored providers are considered only if we found no "normal" providers, * because the "normal" providers are examined again after this loop and should * have precedence over the ignored providers. */ if (!it.hasNext()) { if (ignored == null || !providers.isEmpty()) { // This is the only exit point of the while(true) loop. break; } it = ignored.iterator(); ignored = null; canIgnore = false; } spi = it.next(); if (!ArraysExt.intersects(formatNames, spi.getFormatNames())) { // Not a provider for the format we are looking for. continue; } /* * Ignore the providers that are wrappers around "native" providers, because we * don't want to write metadata like ".prj" or ".tfw" files for every tiles. * However keep trace of the ignored providers, so we can give a second look * at them later if we find no "normal" provider. */ if (canIgnore && Tile.ignore(spi)) { if (ignored == null) { ignored = new ArrayList<>(4); } ignored.add(spi); continue; } } /* * If a suitable writer is found and is capable to encode the output, returns * it immediately. Otherwise the writers are stored in an Set as we found them, * in order to try them again with an ImageOutputStream after this loop. */ if (spi.canEncodeImage(image) && providers.add(spi)) { for (final Class<?> legalType : spi.getOutputTypes()) { if (legalType.isAssignableFrom(outputType)) { final ImageWriter writer = spi.createWriterInstance(); writer.setOutput(output); if (filter(writer)) { if (stream != null) { stream.close(); } cacheEntry.writer = writer; return cacheEntry; } writer.dispose(); break; } } } } /* * No provider accept the output directly. This output is typically a File or URL. * Creates an image output stream from it and try again. */ if (!providers.isEmpty()) { if (stream == null) { stream = ImageIO.createImageOutputStream(output); } if (stream != null) { final Class<? extends ImageOutputStream> streamType = stream.getClass(); for (final ImageWriterSpi spi : providers) { for (final Class<?> legalType : spi.getOutputTypes()) { if (legalType.isAssignableFrom(streamType)) { final ImageWriter writer = spi.createWriterInstance(); writer.setOutput(stream); if (filter(writer)) { cacheEntry.writer = writer; cacheEntry.needStream = true; return cacheEntry; } writer.dispose(); break; } } } } } if (stream != null) { stream.close(); } throw new UnsupportedImageFormatException(Errors.format(Errors.Keys.NoImageWriter)); } /** * Suggests a tile size for the given image size. This is invoked only for image format * that support tiling, for example TIFF. Current implementation search for the first * value equals or greater than 64, which is the tile size used by GDAL. * * @param imageSize The image size. * @return The suggested tile size. */ private static int imageTileSize(final int imageSize) { final int[] divisors = MathFunctions.divisors(imageSize); int i = Arrays.binarySearch(divisors, IMAGE_TILE_SIZE); if (i < 0) i = ~i; return (i < divisors.length) ? divisors[i] : imageSize; } /** * Returns the default stream metadata, or {@code null} if none. * The default implementation returns {@code null} in all cases. */ @Override 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. */ @Override 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. */ @Override 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. */ @Override public IIOMetadata convertImageMetadata(IIOMetadata inData, ImageTypeSpecifier imageType, ImageWriteParam param) { return null; } /** * Deletes all temporary files. */ private void deleteTemporaryFiles() { for (final Iterator<RawFile> it=temporaryFiles.values().iterator(); it.hasNext();) { final Path file = it.next().file; if (!TemporaryFile.delete(file)) { IOUtilities.deleteOnExit(file); } it.remove(); } } /** * Resets this writer to its initial state. If there is any temporary files, * they will be deleted. */ @Override public void reset() { deleteTemporaryFiles(); super.reset(); } /** * Disposes resources held by this writer. 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() { deleteTemporaryFiles(); super.dispose(); } // It is not strictly necessary to override finalize() since ThreadPoolExecutor // already invokes shutdown() in its own finalize() method. /** * Guesses the compression ratio for the given image format. This is very approximative * and used only in order to have some order of magnitude. * * @param spi The image reader or writer provider. * @return The inverse of a guess of compression ratio (1 is uncompressed), or 0 if unknown. */ public static int guessCompressionRatio(final ImageReaderWriterSpi spi) { if (spi != null) { for (final String format : spi.getFormatNames()) { if (format.equalsIgnoreCase("png")) { return 4; } if (format.equalsIgnoreCase("jpeg")) { return 8; } if (format.equalsIgnoreCase("tiff")) { // TIFF are uncompressed unless explicitly specified. return 1; } if (format.equalsIgnoreCase("bmp") || format.equalsIgnoreCase("raw")) { return 1; } } } return 0; } /** * Guesses the number of bits per pixel for an image to be saved on the disk in the RAW format. * Current implementation ignores the leading or trailing bits which could exists on each lines. * If the information is unknown (which may happen with some JPEG readers), conservatively * returns the size required for storing ARGB values using bytes. * * @param type The color model and sample model of the image to be saved. * @return Expected number of bits per pixel. */ public static int bitsPerPixel(final ImageTypeSpecifier type) { if (type != null) { final SampleModel sm = type.getSampleModel(); if (sm != null) { int size = 0; for (final int s : sm.getSampleSize()) { size += s; } return size; } } return 4 * Byte.SIZE; } /** * Service provider for {@link MosaicImageWriter}. This service provider is not strictly * compliant with the Image I/O specification since it cant not work with * {@link javax.imageio.stream.ImageOutputStream}. * * @author Martin Desruisseaux (Geomatys) * @author Cédric Briançon (Geomatys) * @version 3.18 * * @since 2.5 * @module */ public static class Spi extends ImageWriterSpi { /** * The default instance. There is no instance of this provider registered in the * standard {@link javax.imageio.spi.IIORegistry}, because this provider is not * strictly compliant with the Image I/O requirement (in particular, the Mosaic * Image Writer does not accept {@link javax.imageio.stream.ImageOutputStream}). * This constant can be used as a replacement. */ public static final Spi DEFAULT = new Spi(); /** * Creates a default provider. This constructor does not set the {@link #outputTypes} * field in order to delay loading of {@link Tile} and {@link TileManager} classes * as much as possible. */ public Spi() { vendorName = "Geotoolkit.org"; version = Utilities.VERSION.toString(); names = MosaicImageReader.Spi.NAMES; pluginClassName = "org.geotoolkit.image.io.mosaic.MosaicImageWriter"; } /** * Returns the types of objects that may be used as arguments to the * {@link MosaicImageWriter#setOutput(Object)} method. This method * initializes the {@link #outputTypes} field when first needed. * * @since 3.18 */ @Override public synchronized Class<?>[] getOutputTypes() { if (outputTypes == null) { // Initializes the field only when first needed in order to // delay the class loading of TileManager and Tile classes. outputTypes = new Class<?>[] { TileManager[].class, // Preferred type. TileManager.class, Tile[].class, Collection.class // No File type, because directories of tiles are not expected to exist // (after all, this is MosaicImageWriter job to create it!). }; } return super.getOutputTypes(); } /** * Returns {@code true} if the image writer can encode the given output. The default * implementation returns {@code true} if the given object is an instance assignable * to one of the types returned by the {@linkplain #getOutputTypes()} implementation * of this {@code Spi} class, and other type-specific restrictions are meet (e.g. * {@link Collection} contains only instances of {@link Tile}, <i>etc.</i>). * * @param destination The output to be encoded. * @return {@code true} If the image writer can encode the given output. * @throws IOException If an I/O operation was required and failed. * * @since 3.14 */ public boolean canEncodeOutput(final Object destination) throws IOException { return MosaicImageReader.Spi.DEFAULT.canDecodeInput(destination); } /** * 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. */ @Override public boolean canEncodeImage(ImageTypeSpecifier type) { return true; } /** * Returns a new {@link MosaicImageWriter}. * * @throws IOException If an I/O operation was required and failed. */ @Override public ImageWriter createWriterInstance(Object extension) throws IOException { return new MosaicImageWriter(this); } /** * Returns a brief, human-readable description of this service provider. * * @todo Localize. */ @Override public String getDescription(final Locale locale) { return "Mosaic Image Writer"; } } }