/* * Geotoolkit.org - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2009-2012, Open Source Geospatial Foundation (OSGeo) * (C) 2009-2012, Geomatys * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package org.geotoolkit.image.io; import java.nio.file.Files; import java.nio.file.Path; import java.util.Set; import java.util.List; import java.util.Arrays; import java.util.ArrayList; import java.util.Collection; import java.util.IdentityHashMap; import java.util.Iterator; import java.io.File; import java.io.IOException; import java.io.FileNotFoundException; import javax.imageio.ImageIO; import javax.imageio.ImageReader; import javax.imageio.ImageWriter; import javax.imageio.IIOException; import javax.imageio.spi.ServiceRegistry; import javax.imageio.spi.IIORegistry; import javax.imageio.spi.ImageReaderSpi; import javax.imageio.spi.ImageWriterSpi; import javax.imageio.spi.ImageReaderWriterSpi; import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.ImageOutputStream; import java.awt.image.RenderedImage; import java.util.logging.Level; import java.util.logging.Logger; import org.geotoolkit.coverage.io.CoverageIO; import org.geotoolkit.lang.Static; import org.apache.sis.util.ArraysExt; import org.geotoolkit.util.collection.DeferringIterator; import org.geotoolkit.util.collection.FrequencySortedSet; import org.geotoolkit.image.io.mosaic.MosaicImageReader; import org.geotoolkit.image.io.plugin.WorldFileImageReader; import org.geotoolkit.nio.IOUtilities; import org.geotoolkit.internal.image.io.Formats; import org.geotoolkit.factory.Factories; import org.geotoolkit.resources.Errors; import static org.apache.sis.util.ArgumentChecks.ensureNonNull; import org.apache.sis.util.logging.Logging; /** * Extensions to the set of static methods provided in the standard {@link ImageIO} class. * The methods defined in this {@code XImageIO} class fetch an {@link ImageReader} or an * {@link ImageWriter} for a given input, output, suffix, format name or mime type. They * are equivalent in purpose to the methods defined in {@code ImageIO} except that: * <p> * <ul> * <li>Return a single {@code ImageReader} or {@code ImageWriter} instead than an iterator.</li> * <li>Throw an {@link IIOException} if no reader or writer is found.</li> * <li>Accept an optional input or output argument, in order to check if the reader or writer * can use directly that argument instead than creating an input or output stream.</li> * <li>Create automatically an {@linkplain ImageInputStream image input} or * {@linkplain ImageOutputStream output stream} if needed.</li> * </ul> * <p> * For example, the standard Java API provides the following method: * * <blockquote> * {@link ImageIO#getImageReadersBySuffix(String)} with {@code suffix} argument * </blockquote> * * while this class provides the following equivalent API: * * <blockquote> * {@link #getReaderBySuffix(String, Object, Boolean, Boolean)} with {@code suffix}, * {@code input}, {@code seekForwardOnly} and {@code ignoreMetadata} arguments. * </blockquote> * * {@note <b>Why <code>XImageIO</code> methods expect an optional input argument:</b> * The standard <code>ImageIO</code> methods do not take input argument because the Java Image I/O * framework expects every <code>ImageReader</code>s to accept an <code>ImageInputStream</code>. * Actually, <code>ImageIO</code> creates image input streams unconditionally, without checking * whatever it was necessary or not. * <p> * However, some plugins used by the Geotk library can not work with streams. Some plugins (e.g. * the HDF format) open the file in native C/C++ code. Those libraries can not work with Java * streams - they require a plain filename. Even in pure Java code, sometime a filename or URL * is required. For example the Geotk <code>WorldFileImageReader</code> needs to open many files * for the same image. Consequently if the user wants to read an image from a <code>File</code>, * it is preferable to check if an <code>ImageReader</code> accepts directly such <code>File</code> * input before to create an <code>ImageInputStream</code>.} * * {@section How the input/output is defined in the returned reader/writer} * The {@link ImageReader} and {@link ImageWriter} returned by the methods in this class will have * their input or output set. This is different than the standard {@code ImageIO} API (which returns * uninitialized readers or writers) and is done that way because the input or output may be different * than the one specified by the caller if it was necessary to create an {@link ImageInputStream} or * {@link ImageOutputStream}. If such stream has been created, then it is the caller responsibility * to close it after usage. The {@link #close(ImageReader)} and {@link #close(ImageWriter)} * convenience methods can be used for this purpose. * <p> * The output in the returned image writer is defined by a call to the * {@link ImageWriter#setOutput(Object)} method. The input in the returned * image reader is defined by a call to the {@link ImageReader#setInput(Object)}, * {@link ImageReader#setInput(Object, boolean) setInput(Object, boolean)} or * {@link ImageReader#setInput(Object, boolean, boolean) setInput(Object, boolean, boolean)} * method depending on whatever the {@code seekForwardOnly} and {@code ignoreMetadata} arguments * are non-null or not. For example if those two {@link Boolean} arguments are null, then the * {@code setInput(Object)} flavor is invoked. This is usually equivalent to setting the two * boolean argument to {@code false}, but this behavior could have been overridden by the plugin. * * {@section Example} * The following example reads a TIFF image. Because we use an input of type {@link File} * instead than {@link ImageInputStream}, the {@link WorldFileImageReader} can be used. * Consequently the metadata associated with the TIFF image can contain geolocalization * information if a {@code ".tfw"} file was found together with the {@code ".tiff"} file. * * {@preformat java * File input = new File("my_image.tiff"); * ImageReader reader = XImageIO.getReaderBySuffix(input, true, false); * IIOMetadata metadata = reader.getImageMetadata(0); * BufferedImage image = reader.read(0); * XImageIO.close(reader); * reader.dispose(); * } * * {@section Mandatory and optional arguments} * Every methods defined in this class expect exactly one mandatory argument, which is always * the first argument. All other arguments are optional and can be {@code null}. * * @author Martin Desruisseaux (Geomatys) * @version 3.20 * * @since 3.07 * @module */ public final class XImageIO extends Static { private static final Logger LOGGER = Logging.getLogger("org.geotoolkit.image"); /** * Used in {@code switch} statements for selecting the method to invoke * for choosing an image reader or writer. */ private static final int NAME=0, SUFFIX=1, MIME=2; /** * Hack to avoid default java tiff reader. * Provider reader ordonnancement is aleatory. */ private static final String JAI_TIFF_READER_HACK = "com.sun.media.imageioimpl.plugins.tiff.TIFFImageReaderSpi"; /** * Do not allow instantiation of this class. */ private XImageIO() { } /** * Returns the names, suffixes of mime types of the given provider * depending on the value of the {@code mode} argument. * * @param spi The provider from which to get the identifiers. * @param mode Either {@link #NAME}, {@link #SUFFIX} or {@link #MIME}. * @return The requested identifiers. */ static String[] getIdentifiers(final ImageReaderWriterSpi spi, final int mode) { switch (mode) { case NAME: return spi.getFormatNames(); case SUFFIX: return spi.getFileSuffixes(); case MIME: return spi.getMIMETypes(); default: throw new AssertionError(mode); } } /** * Returns an iterator over all providers of the given category having the given name, * suffix or MIME type. * * @param <T> The compile-time type of the {@code category} argument. * @param category Either {@link ImageReaderSpi} or {@link ImageWriterSpi}. * @param mode Either {@link #NAME}, {@link #SUFFIX} or {@link #MIME}. * @param suffix The name, suffix or MIME type to look for, or {@code null}. * @return An iterator over the requested providers. */ private static <T extends ImageReaderWriterSpi> Iterator<T> getServiceProviders( final Class<T> category, final int mode, final String name) { final IIORegistry registry = IIORegistry.getDefaultInstance(); if (name == null) { return orderForClassLoader(registry.getServiceProviders(category, true)); } /* * This inner class filters the providers according the name given in argument to this method, * ignoring cases. However this filter will opportunistically keep trace of providers for which * the lower/upper cases don't match. We do that because the Image I/O API seems to be usually * case-sensitive (for example the PNG format declares both the ".png" and ".PNG" suffixes), * while some formats seem to forgot to declare the two variants (for example the TIFF format * declares the ".tiff" suffix but not the ".TIFF" one). As a safety, we will give precedence * to providers for which an exact match is found before to accept a provider which match only * when ignoring cases. */ @SuppressWarnings("serial") final class Filter extends IdentityHashMap<Object,Object> implements IIORegistry.Filter { @Override public boolean filter(final Object provider) { final String[] identifiers = getIdentifiers((ImageReaderWriterSpi) provider, mode); boolean found = ArraysExt.contains(identifiers, name); if (!found) { found = ArraysExt.containsIgnoreCase(identifiers, name); if (found) { // Remember that this provider matches only when ignoring case. put(provider, null); } } return found; } } /* * At this point, the map is still empty. It may be filled during the iteration process. * Any element stored in the map will be deferred to the end of the iteration. */ final Filter filter = new Filter(); return new DeferringIterator<T>(orderForClassLoader(registry.getServiceProviders(category, filter, true))) { @Override protected boolean isDeferred(final T element) { return filter.containsKey(element); } }; } /** * Wraps the given iterator in order to give precedence to providers loaded by this * application class loader, before providers loaded by class loaders of other applications. * * @since 3.20 */ private static <T extends ImageReaderWriterSpi> Iterator<T> orderForClassLoader(final Iterator<T> iterator) { return Factories.orderForClassLoader(XImageIO.class.getClassLoader(), iterator); } /** * Returns a code indicating which kind of input/output the given reader/writer accepts. * The meaning of the return value are: * <p> * 0: No input/output fit.<br> * 1: The given input/output type can be used directly.<br> * 2: An image input/output stream can be used.</br> * * @param allowedTypes The types allowed by the image reader/writer. * @param givenType The type of the input/output given by the user. * @param streamType The type of the image input/output stream. * @return A code indicating which kind of input is accepted. */ private static int codeAllowedType(final Class<?>[] allowedTypes, final Class<?> givenType, final Class<? extends ImageInputStream> streamType) { int foundStream = 0; if (allowedTypes != null) { for (final Class<?> allowedType : allowedTypes) { if (allowedType.isAssignableFrom(givenType)) { return 1; // The given type can be used directly. } if (allowedType.isAssignableFrom(streamType)) { foundStream = 2; // Continue the search in case the given type can be used directly. } } } return foundStream; } /** * Creates a new reader from the given provider, and initializes its input to the given value. * The {@code seekForwardOnly} and {@code ignoreMetadata} parameters are used only if they are * non-null, otherwise the plugin-dependent default is used. * * @param spi The provider to use for creating a new reader instance. * @param input The input to be given to the new reader instance. * @param seekForwardOnly If {@code true}, images and metadata may only be read in ascending * order from the input source. * @param ignoreMetadata If {@code true}, metadata may be ignored during reads. * @return The new image reader instance with its input initialized. * @throws IOException If an error occurred while creating the image reader instance. */ private static ImageReader createReaderInstance(final ImageReaderSpi spi, final Object input, Boolean seekForwardOnly, Boolean ignoreMetadata) throws IOException { final ImageReader reader = spi.createReaderInstance(); if (input != null) { if (ignoreMetadata != null) { reader.setInput(input, (seekForwardOnly != null) && seekForwardOnly, ignoreMetadata); } else if (seekForwardOnly != null) { reader.setInput(input, seekForwardOnly); } else { reader.setInput(input); } } return reader; } /** * Creates a new reader for the given input. The {@code seekForwardOnly} and * {@code ignoreMetadata} parameters are used only if they are non-null, otherwise * the plugin-dependent default is used. * * @param mode Either {@link #NAME}, {@link #SUFFIX} or {@link #MIME}. * @param name The name, suffix or MIME type to look for, or {@code null}. * @param input The input to be given to the new reader instance, or {@code null} if none. * @param seekForwardOnly If {@code true}, images and metadata may only be read in ascending * order from the input source. * @param ignoreMetadata If {@code true}, metadata may be ignored during reads. * @return The new image reader instance with its input initialized. * @throws IOException If an error occurred while creating the image reader instance. */ private static ImageReader getReader(final int mode, final String name, final Object input, Boolean seekForwardOnly, Boolean ignoreMetadata) throws IOException { List<ImageReaderSpi> usingImageInputStream = null; // Will be created only if needed. Iterator<ImageReaderSpi> it = getServiceProviders(ImageReaderSpi.class, mode, name); boolean hasFound = false; while (it.hasNext()) { final ImageReaderSpi spi = it.next(); //-- to avoid unexpected jai tiff reader if (JAI_TIFF_READER_HACK.equalsIgnoreCase(spi.getClass().getName())) continue; if (Boolean.TRUE.equals(ignoreMetadata)) { /* * If the caller is not interested in metadata, avoid the WorldFileImageReader.Spi * wrapper in order to avoid the cost of reading the TFW/PRJ files. We will rather * use directly the wrapped reader, which should be somewhere next in the iteration. */ if (spi instanceof ImageReaderAdapter.Spi) { final Set<InformationType> info = ((ImageReaderAdapter.Spi) spi).getModifiedInformation(input); if (!info.contains(InformationType.RASTER) && !info.contains(InformationType.IMAGE)) { // Actually, skips the adapter only if it does not modify the pixel values. continue; } } } if (input == null || spi.canDecodeInput(input)) { return createReaderInstance(spi, input, seekForwardOnly, ignoreMetadata); } /* * The Spi has correct format name, MIME type or suffix but claims to be unable * to decode the given input. If the input was not already an ImageInputStream, * remember that Spi so we can try it again with an ImageInputStream later. */ hasFound = true; if (input instanceof ImageInputStream) { continue; } // We accept only case 2 below (not case 1) because if the // given type was a legal type, it should have succeed above. if (codeAllowedType(spi.getInputTypes(), input.getClass(), ImageInputStream.class) == 2) { if (usingImageInputStream == null) { usingImageInputStream = new ArrayList<>(2); } usingImageInputStream.add(spi); } } /* * No Spi accept directly the given input. If at least one Spi accepts an * ImageInputStream, create the stream and check again. */ if (usingImageInputStream != null) { try { final ImageInputStream stream = CoverageIO.createImageInputStream(input); it = usingImageInputStream.iterator(); while (it.hasNext()) { final ImageReaderSpi spi = it.next(); //-- to avoid unexpected jai tiff reader if (JAI_TIFF_READER_HACK.equalsIgnoreCase(spi.getClass().getName())) continue; if (spi.canDecodeInput(stream)) { return createReaderInstance(spi, stream, seekForwardOnly, ignoreMetadata); } } //close stream if no spi support created stream stream.close(); } catch (IOException e) { /* * The stream can not be created. It may be because the file doesn't exist. * From this point we consider that the operation failed, but we need to * build an error message as helpful as possible. */ if (input instanceof File || input instanceof Path) { Path file = (input instanceof File) ? ((File) input).toPath() : (Path) input; short messageKey = Errors.Keys.FileDoesNotExist_1; Path parent; while ((parent = file.getParent()) != null && !Files.isDirectory(parent)) { messageKey = Errors.Keys.NotADirectory_1; file = parent; } throw new FileNotFoundException(Errors.format(messageKey, file)); } else { final short messageKey; final Object argument; if (IOUtilities.canProcessAsPath(input)) { messageKey = Errors.Keys.CantReadFile_1; argument = IOUtilities.filename(input); } else { messageKey = Errors.Keys.UnknownType_1; argument = input.getClass(); } throw new IIOException(Errors.format(messageKey, argument)); } } } throw new UnsupportedImageFormatException(errorMessage(false, mode, name, hasFound)); } /** * Creates an error message for an {@link ImageReader} or {@link ImageWriter} not found. * * @param write {@code true} for a message appropriate for writers, or {@code false} for readers. * @param mode Either {@link #NAME}, {@link #SUFFIX} or {@link #MIME}. * @param name The name, suffix or MIME type to look for, or {@code null}. * @param hasFound {@code true} if at least one reader or writer was found. * @return The error message to declare in exception constructor. */ private static String errorMessage(final boolean write, final int mode, final String name, final boolean hasFound) { if (name == null || hasFound) { return Errors.format(write ? Errors.Keys.NoImageWriter : Errors.Keys.NoImageReader); } final short key; String[] choices; switch (mode) { case NAME: { key = Errors.Keys.UnknownImageFormat_1; choices = write ? ImageIO.getWriterFormatNames() : ImageIO.getReaderFormatNames(); break; } case SUFFIX: { key = Errors.Keys.UnknownFileSuffix_1; choices = write ? ImageIO.getWriterFileSuffixes() : ImageIO.getReaderFileSuffixes(); break; } case MIME: { key = Errors.Keys.UnknownMimeType_1; choices = write ? ImageIO.getWriterMIMETypes() : ImageIO.getReaderMIMETypes(); break; } default: throw new AssertionError(mode); } choices = Formats.simplify(choices); final boolean hasChoices = (choices != null && choices.length != 0); final Errors resources = Errors.getResources(null); String message; if (mode == NAME && hasChoices) { message = resources.getString(Errors.Keys.NoImageFormat_2, name, Arrays.toString(choices)); } else { message = resources.getString(key, name); if (hasChoices) { message = message + ' ' + resources.getString(Errors.Keys.ExpectedOneOf_1, Arrays.toString(choices)); } } return message; } /** * Creates a new reader for the given input. The {@code input} argument is mandatory and will be * used for initializing the reader by a call to one of the {@link ImageReader#setInput(Object)} * methods. The method invoked depends on whatever the {@code seekForwardOnly} and * {@code ignoreMetadata} parameters are null or non-null. * * @param input * The mandatory input to be given to the new reader instance. * @param seekForwardOnly * Optional parameter to be given (if non-null) to the * {@link ImageReader#setInput(Object, boolean) setInput} method: if {@code true}, * images and metadata may only be read in ascending order from the input source. * If {@code false}, they may be read in any order. If {@code null}, this parameter * is not given to the reader which is free to use a plugin-dependent default * (usually {@code false}). * @param ignoreMetadata * Optional parameter to be given (if non-null) to the * {@link ImageReader#setInput(Object, boolean, boolean) setInput} method: * if {@code true}, metadata may be ignored during reads. If {@code false}, metadata * will be parsed. If {@code null}, this parameter is not given to the reader which * is free to use a plugin-dependent default (usually {@code false}). * @return The new image reader instance with its input initialized. * @throws IOException If no suitable image reader has been found, or if an error occurred * while creating it. * * @see ImageIO#getImageReaders(Object) */ public static ImageReader getReader(final Object input, final Boolean seekForwardOnly, final Boolean ignoreMetadata) throws IOException { ensureNonNull("input", input); if (MosaicImageReader.Spi.DEFAULT.canDecodeInput(input)) { return createReaderInstance(MosaicImageReader.Spi.DEFAULT, input, seekForwardOnly, ignoreMetadata); } return getReader(0, null, input, seekForwardOnly, ignoreMetadata); } /** * Creates a new reader for the given input, considering only the readers that claim to decode * files having the suffix found in the input. The {@code input} argument is mandatory and will * be used for initializing the reader as documented in the {@link #getReader getReader(...)} * method. * <p> * If this method doesn't know how to get the suffix from the given input (for example * if the input is an instance of {@link ImageInputStream}), then this method delegates * to {@link #getReader(Object, Boolean, Boolean)}. * * @param input The mandatory input to be given to the new reader instance. * @param seekForwardOnly Optional parameter to be given (if non-null) to the * {@link ImageReader#setInput(Object, boolean) setInput} method. * @param ignoreMetadata Optional parameter to be given (if non-null) to the * {@link ImageReader#setInput(Object, boolean, boolean) setInput} method. * @return The new image reader instance with its input initialized. * @throws IOException If no suitable image reader has been found, or if an error occurred * while creating it. * * @see ImageIO#getImageReadersBySuffix(String) */ public static ImageReader getReaderBySuffix(final Object input, final Boolean seekForwardOnly, final Boolean ignoreMetadata) throws IOException { ensureNonNull("input", input); if (!IOUtilities.canProcessAsPath(input)) { return getReader(input, seekForwardOnly, ignoreMetadata); } return getReaderBySuffix(IOUtilities.extension(input), input, seekForwardOnly, ignoreMetadata); } /** * Creates a new reader for the given optional input, considering only the readers that claim * to decode files having the given suffix. Only the {@code suffix} argument is mandatory. The * other ones are optional and will be used (if non-null) for initializing the reader as * documented in the {@link #getReader getReader(...)} method. * * @param suffix The file suffix for which we want a reader. * @param input An optional input to be given to the new reader instance, or {@code null} if none. * @param seekForwardOnly Optional parameter to be given (if non-null) to the * {@link ImageReader#setInput(Object, boolean) setInput} method. * @param ignoreMetadata Optional parameter to be given (if non-null) to the * {@link ImageReader#setInput(Object, boolean, boolean) setInput} method. * @return The new image reader instance with its input initialized (if {@code input} is not null). * @throws IOException If no suitable image reader has been found, or if an error occurred * while creating it. * * @see ImageIO#getImageReadersBySuffix(String) */ public static ImageReader getReaderBySuffix(final String suffix, final Object input, final Boolean seekForwardOnly, final Boolean ignoreMetadata) throws IOException { ensureNonNull("suffix", suffix); return getReader(SUFFIX, suffix, input, seekForwardOnly, ignoreMetadata); } /** * Creates a new reader for the given optional input, considering only the readers of the given * format name. Only the {@code name} argument is mandatory. The other ones are optional and * will be used (if non-null) for initializing the reader as documented in the * {@link #getReader getReader(...)} method. * * @param name The name of the format looked for. * @param input An optional input to be given to the new reader instance, or {@code null} if none. * @param seekForwardOnly Optional parameter to be given (if non-null) to the * {@link ImageReader#setInput(Object, boolean) setInput} method. * @param ignoreMetadata Optional parameter to be given (if non-null) to the * {@link ImageReader#setInput(Object, boolean, boolean) setInput} method. * @return The new image reader instance with its input initialized (if {@code input} is not null). * @throws IOException If no suitable image reader has been found, or if an error occurred * while creating it. * * @see ImageIO#getImageReadersByFormatName(String) */ public static ImageReader getReaderByFormatName(final String name, final Object input, final Boolean seekForwardOnly, final Boolean ignoreMetadata) throws IOException { ensureNonNull("name", name); return getReader(NAME, name, input, seekForwardOnly, ignoreMetadata); } /** * Creates a new reader for the given optional input, considering only the readers for the * given MIME type. Only the {@code mime} argument is mandatory. The other ones are optional * and will be used (if non-null) for initializing the reader as documented in the * {@link #getReader getReader(...)} method. * * @param mime The MIME type of the format looked for. * @param input An optional input to be given to the new reader instance, or {@code null} if none. * @param seekForwardOnly Optional parameter to be given (if non-null) to the * {@link ImageReader#setInput(Object, boolean) setInput} method. * @param ignoreMetadata Optional parameter to be given (if non-null) to the * {@link ImageReader#setInput(Object, boolean, boolean) setInput} method. * @return The new image reader instance with its input initialized (if {@code input} is not null). * @throws IOException If no suitable image reader has been found, or if an error occurred * while creating it. * * @see ImageIO#getImageReadersByMIMEType(String) */ public static ImageReader getReaderByMIMEType(final String mime, final Object input, final Boolean seekForwardOnly, final Boolean ignoreMetadata) throws IOException { ensureNonNull("mime", mime); return getReader(MIME, mime, input, seekForwardOnly, ignoreMetadata); } /** * Closes the input stream of the given reader, and * {@linkplain ImageReader#setInput(Object) sets the input} to {@code null}. * * @param reader The reader for which to close the input stream. * @throws IOException If an error occurred while closing the stream. */ public static void close(final ImageReader reader) throws IOException { ensureNonNull("reader", reader); final Object input = reader.getInput(); reader.setInput(null); IOUtilities.close(input); } /** * Like {@link #close(ImageReader)} this method close the input stream of the given reader * but also dispose ImageReader itself. * * @param reader The reader for which to close the input stream. * @throws IOException If an error occurred while closing the stream. */ public static void dispose(final ImageReader reader) throws IOException { close(reader); reader.dispose(); } /** * Like {@link #dispose(ImageReader)} this method close the input stream of the given reader * but also dispose ImageReader itself without throwing an exception. * * @param reader reader to dispose * @return dispose status. {@code true} if dispose succeed, {@code false} otherwise */ public static boolean disposeSilently(final ImageReader reader) { if(reader == null) return true; try { dispose(reader); return true; } catch (IOException e) { LOGGER.log(Level.FINER, e.getLocalizedMessage(), e); return false; } } /** * Creates a new writer from the given provider, and initializes its output to the given value. * * @param spi The provider to use for creating a new writer instance. * @param output The output to be given to the new writer instance. * @return The new image writer instance with its output initialized. * @throws IOException If an error occurred while creating the image writer instance. */ private static ImageWriter createWriterInstance(final ImageWriterSpi spi, final Object output) throws IOException { final ImageWriter writer = spi.createWriterInstance(); if (output != null) { writer.setOutput(output); } return writer; } /** * Creates a new writer for the given output. * * @param mode Either {@link #NAME}, {@link #SUFFIX} or {@link #MIME}. * @param name The name, suffix or MIME type to look for, or {@code null}. * @param output The output to be given to the new writer instance. * @param image The image to encode, or {@code null} if unknown. * @return The new image writer instance with its output initialized. * @throws IOException If an error occurred while creating the image writer instance. */ private static ImageWriter getWriter(final int mode, final String name, final Object output, final RenderedImage image) throws IOException { ImageWriterSpi fallback = null; Iterator<ImageWriterSpi> it = getServiceProviders(ImageWriterSpi.class, mode, name); boolean hasFound = false; while (it.hasNext()) { hasFound = true; final ImageWriterSpi spi = it.next(); if (image == null || spi.canEncodeImage(image)) { if (output == null) { return createWriterInstance(spi, output); } switch (codeAllowedType(spi.getOutputTypes(), output.getClass(), ImageOutputStream.class)) { /* * The Spi can write directly in the given output. */ case 1: { return createWriterInstance(spi, output); } /* * The Spi has correct format name, MIME type or suffix but claims to be unable * to encode the given output. If the output was not an ImageOutputStream, * remember that Spi so we can try it again with an ImageOutputStream later. */ case 2: { if (fallback == null && !(output instanceof ImageOutputStream)) { fallback = spi; } break; } } } } /* * No Spi accept directly the given output. If at least one Spi accepts an * ImageOutputStream, create the stream and check again. */ if (fallback != null) { final ImageOutputStream stream = ImageIO.createImageOutputStream(output); if (stream != null) { return createWriterInstance(fallback, stream); } } throw new UnsupportedImageFormatException(errorMessage(true, mode, name, hasFound)); } /** * Creates a new writer for the given output, considering only the writers that claim to * encode files having the suffix of the given output. If a writer is found, then the writer * will be initialized to the given output by a call to its * {@link ImageWriter#setOutput setOutput} method. * * @param output A mandatory output to be given to the new writer instance. * @param image The image to encode, or {@code null} if unknown. * @return The new image writer instance with its output initialized. * @throws IOException If no suitable image writer has been found, or if an error occurred * while creating it. * * @see ImageIO#getImageWritersBySuffix(String) */ public static ImageWriter getWriterBySuffix(final Object output, final RenderedImage image) throws IOException { ensureNonNull("output", output); return getWriterBySuffix(IOUtilities.extension(output), output, image); } /** * Creates a new writer for the given output, considering only the writers that claim to * encode files having the given suffix. If a writer is found and the given output is * non-null, then the writer will be initialized to the given output by a call to its * {@link ImageWriter#setOutput setOutput} method. * * @param suffix The file suffix for which we want a writer. * @param output An optional output to be given to the new writer instance, or {@code null} if none. * @param image The image to encode, or {@code null} if unknown. * @return The new image writer instance with its output initialized (if {@code output} is not null). * @throws IOException If no suitable image writer has been found, or if an error occurred * while creating it. * * @see ImageIO#getImageWritersBySuffix(String) */ public static ImageWriter getWriterBySuffix(final String suffix, final Object output, final RenderedImage image) throws IOException { ensureNonNull("suffix", suffix); return getWriter(SUFFIX, suffix, output, image); } /** * Creates a new writer for the given output, considering only the writers of the given format * name. If a writer is found and the given output is non-null, then the writer will be * initialized to the given output by a call to its {@link ImageWriter#setOutput setOutput} * method. * * @param name The format name for which we want a writer. * @param output An optional output to be given to the new writer instance, or {@code null} if none. * @param image The image to encode, or {@code null} if unknown. * @return The new image writer instance with its output initialized (if {@code output} is not null). * @throws IOException If no suitable image writer has been found, or if an error occurred * while creating it. * * @see ImageIO#getImageWritersByFormatName(String) */ public static ImageWriter getWriterByFormatName(final String name, final Object output, final RenderedImage image) throws IOException { ensureNonNull("name", name); return getWriter(NAME, name, output, image); } /** * Creates a new writer for the given output, considering only the writers for the given MIME * type. If a writer is found and the given output is non-null, then the writer will be * initialized to the given output by a call to its {@link ImageWriter#setOutput setOutput} * method. * * @param mime The MIME type for which we want a writer. * @param output An optional output to be given to the new writer instance, or {@code null} if none. * @param image The image to encode, or {@code null} if unknown. * @return The new image writer instance with its output initialized (if {@code output} is not null). * @throws IOException If no suitable image writer has been found, or if an error occurred * while creating it. * * @see ImageIO#getImageWritersByMIMEType(String) */ public static ImageWriter getWriterByMIMEType(final String mime, final Object output, final RenderedImage image) throws IOException { ensureNonNull("mime", mime); return getWriter(MIME, mime, output, image); } /** * Closes the output stream of the given writer, and * {@linkplain ImageWriter#setOutput(Object) sets the output} to {@code null}. * * @param writer The writer for which to close the output stream. * @throws IOException If an error occurred while closing the stream. */ public static void close(final ImageWriter writer) throws IOException { ensureNonNull("writer", writer); final Object output = writer.getOutput(); writer.setOutput(null); IOUtilities.close(output); } /** * Like {@link #close(ImageWriter)} this method close the input stream of the given writer * but also dispose ImageWriter itself. * * @param writer The writer for which to close the input stream. * @throws IOException If an error occurred while closing the stream. */ public static void dispose(final ImageWriter writer) throws IOException { close(writer); writer.dispose(); } /** * Like {@link #close(ImageWriter)} this method close the input stream of the given writer * but also dispose ImageWriter itself without throwing an exception. * * @param writer The writer for which to close the input stream. * @return dispose status. {@code true} if dispose succeed, {@code false} otherwise */ public static boolean disposeSilently(final ImageWriter writer) { if(writer == null) return true; try { close(writer); writer.dispose(); return true; } catch (IOException e) { LOGGER.log(Level.FINER, e.getLocalizedMessage(), e); return false; } } /** * Returns the provider of {@code ImageReader}s corresponding to the given provider of * {@code ImageWriter}s, or {@code null} if none. This method performs the same work * than {@link ImageIO#getImageReader(ImageWriter)}, except that it works on providers * instead than reader/writer instances. * * @param writerSpi The provider of image writers for which to get the provider of image readers. * Can be {@code null}, in which case this method will return {@code null}. * @return The provider of image readers, or {@code null} if none. * * @see ImageIO#getImageReader(ImageWriter) * @see ImageWriterSpi#getImageReaderSpiNames() * * @since 3.20 */ public static ImageReaderSpi getImageReaderSpi(final ImageWriterSpi writerSpi) { if (writerSpi == null) { return null; } return (ImageReaderSpi) getSpi(writerSpi.getImageReaderSpiNames(), "getImageReaderSpi"); } /** * Returns the provider of {@code ImageWriter}s corresponding to the given provider of * {@code ImageReader}s, or {@code null} if none. This method performs the same work * than {@link ImageIO#getImageWriter(ImageReader)}, except that it works on providers * instead than reader/writer instances. * * @param readerSpi The provider of image readers for which to get the provider of image writers. * Can be {@code null}, in which case this method will return {@code null}. * @return The provider of image writers, or {@code null} if none. * * @see ImageIO#getImageWriter(ImageReader) * @see ImageReaderSpi#getImageWriterSpiNames() * * @since 3.20 */ public static ImageWriterSpi getImageWriterSpi(final ImageReaderSpi readerSpi) { if (readerSpi == null) { return null; } return (ImageWriterSpi) getSpi(readerSpi.getImageWriterSpiNames(), "getImageWriterSpi"); } /** * Implementation of {@link #getImageReaderSpi(ImageWriterSpi)} and * {@link #getImageWriterSpi(ImageReaderSpi)}. */ private static Object getSpi(final String[] names, final String methodName) { if (names != null) { final IIORegistry registry = IIORegistry.getDefaultInstance(); for (final String name : names) { try { final Object spi = registry.getServiceProviderByClass(Class.forName(name)); if (spi != null) { return spi; } } catch (ClassNotFoundException e) { Logging.recoverableException(null, XImageIO.class, methodName, e); } } } return null; } /** * Returns the image reader provider for the given format name. * * @param format The name of the provider to fetch. * @return The reader provider for the given format. * @throws IllegalArgumentException If no provider is found for the given format. */ public static ImageReaderSpi getReaderSpiByFormatName(final String format) { ensureNonNull("format", format); return Formats.getReaderByFormatName(format, null); } /** * Returns the image writer provider for the given format name. * * @param format The name of the provider to fetch. * @return The reader provider for the given format. * @throws IllegalArgumentException If no provider is found for the given format. */ public static ImageWriterSpi getWriterSpiByFormatName(final String format) { ensureNonNull("format", format); return Formats.getWriterByFormatName(format, null); } /** * Returns the preferred suffix declared in the given provider, with a leading dot. * If the given provider is null, or if no preferred file suffix is found, then this * method returns {@code null}. * * @param provider The provider for which to get the first file suffix, or {@code null}. * @return The first file suffix, or {@code null} if none. * * @since 3.20 */ public static String getFileSuffix(final ImageReaderWriterSpi provider) { if (provider != null) { final String[] suffixes = provider.getFileSuffixes(); if (suffixes != null) { for (String suffix : suffixes) { if (suffix != null && !(suffix = suffix.trim()).isEmpty()) { if (suffix.charAt(0) != '.') { suffix = '.' + suffix; } return suffix; } } } } return null; } /** * Returns the format names of all {@link ImageReaderSpi} and/or {@link ImageWriterSpi} * instances registered for the given MIME type. If there is many format names for the * same MIME type, then the most frequently used names will be sorted first. * * @param mime The MIME type to search for. * @param read {@code true} if the format name is required to be supported by at least one {@code ImageReader}. * @param write {@code true} if the format name is required to be supported by at least one {@code ImageWriter}. * @return The format names, or an empty array if none. * * @since 3.14 */ public static String[] getFormatNamesByMimeType(final String mime, final boolean read, final boolean write) { ensureNonNull("mime", mime); final IIORegistry registry = IIORegistry.getDefaultInstance(); final FrequencySortedSet<String> formats = new FrequencySortedSet<>(true); if (read != write) { /* * Caller asked for read support, or write support, but not both. * Query only the appropriate type. */ getFormatNamesByMimeType(registry, mime, read ? ImageReaderSpi.class : ImageWriterSpi.class, formats); } else if (!read) { /* * Read or write support was not explicitly required: returns the union of * all formats, regardless if they are supported by readers or writers. */ getFormatNamesByMimeType(registry, mime, ImageReaderSpi.class, formats); getFormatNamesByMimeType(registry, mime, ImageWriterSpi.class, formats); } else { /* * Caller asked for read and write support. * Computes the intersection of both sets. */ final List<String> readers = new ArrayList<>(); final List<String> writers = new ArrayList<>(); getFormatNamesByMimeType(registry, mime, ImageReaderSpi.class, readers); getFormatNamesByMimeType(registry, mime, ImageWriterSpi.class, writers); // First, add all formats in order to compute their frequencies. formats.addAll(readers); formats.addAll(writers); // Next, compute the intersection. formats.retainAll(readers); formats.retainAll(writers); } return formats.toArray(new String[formats.size()]); } /** * Adds to the given set the format names of every {@link ImageReaderSpi} or * {@link ImageWriterSpi} instances registered for the given MIME type. * * @param registry The registry from which to get the provider. * @param mime The MIME type to search for. * @param type {@link ImageReaderSpi} or {@link ImageWriterSpi}. * @param addTo The set in which to add the format names. */ private static void getFormatNamesByMimeType(final ServiceRegistry registry, final String mime, final Class<? extends ImageReaderWriterSpi> type, final Collection<String> addTo) { final Iterator<? extends ImageReaderWriterSpi> it = registry.getServiceProviders(type, false); while (it.hasNext()) { final ImageReaderWriterSpi spi = it.next(); if (ArraysExt.containsIgnoreCase(spi.getMIMETypes(), mime)) { final String[] names = spi.getFormatNames(); if (names != null) { addTo.addAll(Arrays.asList(names)); } } } } /** * Check if the provided object is an instance of one of the given classes. * Used test if an obect is compatible with {@link ImageWriterSpi#getOutputTypes()} or * {@link ImageReaderSpi#getInputTypes()} supported classes. * * @param validTypes list of supported classes * @param type candidate object * @return true if candidate object class is an instance of supported class. */ public static boolean isValidType(final Class<?>[] validTypes, final Object type) { for (final Class<?> t : validTypes) { if (t.isInstance(type)) { return true; } } return false; } /** * Convert given input to an input support by given spi. * In last case an ImageInputStream will be created. * * @param spi {@link ImageReaderSpi} reader spi * @param input candidate object * @return Input object if directly supported by ImageReaderSpi or input object converted into * an {@link org.geotoolkit.coverage.io.ImageCoverageReader} * @throws IOException if {@link org.geotoolkit.coverage.io.ImageCoverageReader} fail */ public static Object toSupportedInput(ImageReaderSpi spi, Object input) throws IOException{ final Class[] supportedTypes = spi.getInputTypes(); Object in = null; //try to reuse input if it's supported for(Class type : supportedTypes){ if(type.isInstance(input)){ in = input; break; } } //use default image stream if necessary if(in == null){ //may throw IOException in = CoverageIO.createImageInputStream(input); } return in; } /** * Returns the mime type matching the extension of an image file. * For example, for a file "my_image.png" it will return "image/png", in most cases. * * @param extension The extension of an image file. * @return The mime type for the extension specified. * * @throws IIOException if no image reader are able to handle the extension given. */ public static String fileExtensionToMimeType(final String extension) throws IIOException { final Iterator<ImageReaderSpi> readers = IIORegistry.lookupProviders(ImageReaderSpi.class); while (readers.hasNext()) { final ImageReaderSpi reader = readers.next(); final String[] suffixes = reader.getFileSuffixes(); for (String suffixe : suffixes) { if (extension.equalsIgnoreCase(suffixe)) { final String[] mimeTypes = reader.getMIMETypes(); if (mimeTypes != null && mimeTypes.length > 0) { return mimeTypes[0]; } } } } throw new IIOException("No available image reader able to handle the extension specified: "+ extension); } /** * Returns the mime type matching the format name of an image file. * For example, for a format name "png" it will return "image/png", in most cases. * * @param formatName name The format name of an image file. * @return The mime type for the format name specified. * * @throws IIOException if no image reader are able to handle the format name given. */ public static String formatNameToMimeType(final String formatName) throws IIOException { final Iterator<ImageReaderSpi> readers = IIORegistry.lookupProviders(ImageReaderSpi.class); while (readers.hasNext()) { final ImageReaderSpi reader = readers.next(); final String[] formats = reader.getFormatNames(); for (String format : formats) { if (formatName.equalsIgnoreCase(format)) { final String[] mimeTypes = reader.getMIMETypes(); if (mimeTypes != null && mimeTypes.length > 0) { return mimeTypes[0]; } } } } throw new IIOException("No available image reader able to handle the format name specified: "+ formatName); } /** * Returns the format name matching the mime type of an image file. * For example, for a mime type "image/png" it will return "png", in most cases. * * @param mimeType The mime type of an image file. * @return The format name for the mime type specified. * * @throws IIOException if no image reader are able to handle the mime type given. */ public static String mimeTypeToFormatName(final String mimeType) throws IIOException { final Iterator<ImageReaderSpi> readers = IIORegistry.lookupProviders(ImageReaderSpi.class); while (readers.hasNext()) { final ImageReaderSpi reader = readers.next(); final String[] mimes = reader.getMIMETypes(); for (String mime : mimes) { if (mimeType.equalsIgnoreCase(mime)) { final String[] formats = reader.getFormatNames(); if (formats != null && formats.length > 0) { return formats[0]; } } } } throw new IIOException("No available image reader able to handle the mime type specified: "+ mimeType); } }