/* * 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.internal.image.io; import java.util.Arrays; import java.util.Locale; import java.util.List; import java.util.LinkedList; import java.util.Iterator; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import javax.imageio.IIOException; import javax.imageio.ImageIO; import javax.imageio.ImageReader; import javax.imageio.spi.IIORegistry; import javax.imageio.spi.ImageReaderSpi; import javax.imageio.spi.ImageWriterSpi; import javax.imageio.spi.IIOServiceProvider; import javax.imageio.spi.ImageInputStreamSpi; import javax.imageio.spi.ImageReaderWriterSpi; import javax.imageio.stream.ImageInputStream; import org.geotoolkit.coverage.io.CoverageIO; import org.geotoolkit.factory.Factories; import org.geotoolkit.lang.Static; import org.apache.sis.util.ArraysExt; import org.geotoolkit.nio.IOUtilities; import org.geotoolkit.resources.Errors; import org.geotoolkit.resources.Vocabulary; /** * Utility methods about image formats. * * @author Martin Desruisseaux (Geomatys) * @version 3.20 * * @since 3.01 * @module */ public final class Formats extends Static { /** * Do not allow instantiation of this class. */ private Formats() { } /** * A callback for performing an arbitrary operation using an {@link ImageReader} * selected from a given input. An implementation of this interface is given to * {@link Formats#selectImageReader Formats.selectImageReader(...)}. * * @author Martin Desruisseaux (Geomatys) * @version 3.05 * * @since 3.05 * @module */ public interface ReadCall { /** * Invoked when a suitable image reader has been found. If the operation fails with * an {@link IOException}, then {@link Formats#selectImageReader selectImageReader} * will searches for an other image reader. If none are found, then the first exception * will be rethrown. * <p> * This method should not retain a reference to the image reader, because it will be * disposed by the caller after this method call. * * @param reader The image reader. * @throws IOException If an I/O error occurred. */ void read(ImageReader reader) throws IOException; /** * Invoked when a recoverable error occurred. Implementors will typically delegate to * {@link org.apache.sis.util.logging.Logging#recoverableException(Logger, Class, String, Throwable)} * with appropriate class an method name. * * @param error The error which occurred. */ void recoverableException(Throwable error); } /** * Searches {@link ImageReader}s that claim to be able to decode the given input, and call * {@link ReadCall#read(ImageReader)} for each of them until a call succeed. If every * readers fail with an {@link IOException}, the exception of the first reader is rethrown * by this method. * <p> * Every {@link ImageReader} instances created by this method are disposed after usage. * Their input streams (if any) will be closed. * * @param input The input for which image reader are searched. * @param locale The locale to set to the image readers, or {@code null} if none. * @param callback The method to call when an {@link ImageReader} seems suitable. * @throws IOException If no suitable image reader has been found. * * @since 3.05 */ public static void selectImageReader(final Object input, final Locale locale, final ReadCall callback) throws IOException { /* * If the given input is a file, URL or URI with a suffix, extract the suffix. * We will try the ImageReader for that suffix before to try any other readers. */ final String suffix = IOUtilities.extension(input); boolean useSuffix = (suffix != null && !suffix.isEmpty()); int nextProviderForSuffix = 0; // Used for sorting first the providers for the suffix. /* * The list of providers that we have found during the first execution of the * loop below. For every loop execution after the first one, we will use that * list instead of the IIORegistry iterator. */ final List<ImageReaderSpi> providers = new LinkedList<>(); boolean useProvidersList = false; /* * The state of this method (whatever we have found a reader, or failed). */ boolean success = false; IOException failure = null; ImageInputStream stream = null; Object inputOrStream = input; attmpt: while (true) { /* * On first execution, iterate over the provider given by IIORegistry. * For all other execution, iterate over the providers in our list. */ int index = 0; final Iterator<ImageReaderSpi> it = orderForClassLoader(useProvidersList ? providers.iterator() : IIORegistry.getDefaultInstance().getServiceProviders(ImageReaderSpi.class, true)); while (it.hasNext()) { final ImageReaderSpi provider = it.next(); /* * If this is the first iteration, then add the provider in the list. We add * the providers in iteration order, except if the suffixes match in which case * we add the providers at the beginning of the list (so we try them first if we * perform an other iteration later). * * Note: useSuffix and useProvidersList are not modified in this inner loop. */ if (!useProvidersList) { if (!useSuffix) { providers.add(provider); } else if (ArraysExt.contains(provider.getFileSuffixes(), suffix)) { providers.add(nextProviderForSuffix++, provider); } else { providers.add(provider); continue; // Suffixes doesn't match: skip (for now) this provider. } } else if (useSuffix) { /* * This block is executed during the second iteration. We are still trying * the ImageReader for the suffix, but this time using an ImageInputStream * input. Remove the providers since we will not try them again in case of * failure. */ if (index++ == nextProviderForSuffix) { break; // Reached the first provider which doesn't have the right suffix. } it.remove(); } /* * If the provider thinks that the input can't be read, skip it for now. * It may be tried again with a different input in the next loop execution. */ if (!provider.canDecodeInput(inputOrStream)) { continue; } /* * Configure the ImageReader, then tries to read the image. In case of success, * we are done and will exit from the loop. In case of failure, we will report * the error and continue with the next providers. */ final ImageReader reader = provider.createReaderInstance(); if (inputOrStream instanceof ImageInputStream) { ((ImageInputStream) inputOrStream).mark(); } reader.setInput(inputOrStream, true, false); if (locale != null) try { reader.setLocale(locale); } catch (IllegalArgumentException e) { // Unsupported locale. Not a big deal, so ignore... } try { callback.read(reader); success = true; break attmpt; } catch (IOException e) { if (failure == null) { failure = e; } else { failure.addSuppressed(e); } } finally { /* * Reset the stream to its initial state if: * * - There is a failure, because we will need the * stream again for trying the next ImageReaders. * * - Or if the stream was provided by the caller, * because the caller may want to use it again. */ reader.dispose(); if (!success || inputOrStream == input) { if (inputOrStream instanceof ImageInputStream) try { ((ImageInputStream) inputOrStream).reset(); } catch (IOException e) { // If the stream was provided by the caller, we can not // create a new one. So consider the error as fatal. if (inputOrStream == input) { throw e; } // Failed to reset the stream, but we created // it ourself. So let just create an other one. callback.recoverableException(e); ((ImageInputStream) inputOrStream).close(); try { inputOrStream = stream = CoverageIO.createImageInputStream(input); } catch (IOException ioe) { e.addSuppressed(ioe); throw e; } } } } } /* * At this point we finished to iterate over every ImageReader providers, but * we didn't found any suitable one. Switch the function to the next state, * which are in order: * * 1) Only ImageReaders for the suffix, using the input supplied by the caller. * 2) Only ImageReaders for the suffix, using a new ImageInputStream input. * 3) All ImageReaders, using the input supplied by the caller. * 4) All ImageReaders, using the ImageInputStream created at step 2. * 5) End of the attempts: failure. * * The next state is inferred from the previous state. A simple 'switch' statement * using a state number is not convenient because any of the above states may be * skipped, for example if no ImageReaders were found for the suffix (in which case * we try immediately all ImageReaders), or if the caller input is already an * ImageInputStream. */ if (inputOrStream instanceof ImageInputStream) { /* * If we have run the most extensive case (all available * ImageReaders with an ImageInputStream input), give up. */ if (!useSuffix) { break; } /* * The previous run was using a limited set of ImageReaders. Try again, * but now using all remaining ImageReaders. Note that when we switched * the 'useSuffix' flag to 'false', we never switch it back to 'true'. * * If we were using an ImageInputStream, try with the original input. * Note that we keep the ImageInputStream (referenced by the 'stream' * variable) in order to use it again if the upcoming try fails. */ useSuffix = false; if (inputOrStream != input) { inputOrStream = input; } } else { /* * The input can not be used directly. We may need to create an ImageInputStream. * But before doing so, check if we tried at least one ImageReader. If not, try * all ImageReaders with the original input before to create an ImageInputStream. */ if (useSuffix && nextProviderForSuffix == 0) { useSuffix = false; } else { /* * Failed to read the image using the caller input. * Wraps it in an ImageInputStream and try again. */ inputOrStream = stream; if (stream == null) { try { stream = CoverageIO.createImageInputStream(input); inputOrStream = stream; } catch (IOException ioe) { if (!useSuffix) { break; } useSuffix = false; inputOrStream = input; } } } } useProvidersList = true; } /* * We got a success, or we tried every image readers. Close the * stream only if we created it ourself (i.e. inputOrStream != input). */ if (stream != null) { stream.close(); } if (!success) { if (failure == null) { if (input instanceof File && !((File) input).exists()) { failure = new FileNotFoundException(Errors.format(Errors.Keys.FileDoesNotExist_1, input)); } else { failure = new IIOException(Errors.format(Errors.Keys.NoImageReader)); } } throw failure; } } /** * Returns the image reader provider for the given format name. This method prefers * standard readers instead than JAI ones, except for the TIFF format for which the * JAI reader is preferred. * <p> * <b>NOTE:</b> The rule for preferring a reader are the same ones than the rules implemented * by {@link org.geotoolkit.image.jai.Registry#setDefaultCodecPreferences()}. If the rule in * the above methods are modified, the rules in this method shall be modified accordingly. * * @param format The name of the provider to fetch, or {@code null}. * @param exclude Base class of readers to exclude, or {@code null} if none. * @return The reader provider for the given format, or {@code null} if {@code format} is null. * @throws IllegalArgumentException If no provider is found for the given format. */ public static ImageReaderSpi getReaderByFormatName(final String format, final Class<? extends ImageReaderSpi> exclude) throws IllegalArgumentException { return getByFormatName(ImageReaderSpi.class, format, exclude); } /** * Returns the image writer provider for the given format name. This method prefers * standard writers instead than JAI ones, except for the TIFF format for which the * JAI writer is preferred. * <p> * <b>NOTE:</b> The rule for preferring a writer are the same ones than the rules implemented * by {@link org.geotoolkit.image.jai.Registry#setDefaultCodecPreferences()}. If the rule in * the above methods are modified, the rules in this method shall be modified accordingly. * * @param format The name of the provider to fetch, or {@code null}. * @param exclude Base class of writers to exclude, or {@code null} if none. * @return The writer provider for the given format, or {@code null} if {@code format} is null. * @throws IllegalArgumentException If no provider is found for the given format. */ public static ImageWriterSpi getWriterByFormatName(final String format, final Class<? extends ImageWriterSpi> exclude) throws IllegalArgumentException { return getByFormatName(ImageWriterSpi.class, format, exclude); } /** * Implementation of {@link #getReaderByFormatName} and {@link #getWriterByFormatName}. */ private static <T extends ImageReaderWriterSpi> T getByFormatName( final Class<T> type, String format, final Class<? extends T> exclude) throws IllegalArgumentException { if (format == null) { return null; } format = format.trim(); T fallback = null; final boolean preferJAI = format.equalsIgnoreCase("TIFF"); final IIORegistry registry = IIORegistry.getDefaultInstance(); final Iterator<T> it=orderForClassLoader(registry.getServiceProviders(type, true)); while (it.hasNext()) { final T provider = it.next(); if (exclude != null && exclude.isInstance(provider)) { continue; } if (ArraysExt.contains(provider.getFormatNames(), format)) { /* * NOTE: The following method uses the same rule for identifying JAI codecs. * If we change the way to identify those codecs here, we should do the * same for the other method. * * org.geotoolkit.image.jai.Registry.setNativeCodecAllowed(String, Class, boolean) */ if (provider.getClass().getName().startsWith("com.sun.media.") == preferJAI) { return provider; } if (fallback == null) { fallback = provider; } } } if (fallback != null) { return fallback; } throw new IllegalArgumentException(Errors.format(Errors.Keys.UnknownImageFormat_1, format)); } /** * Returns the name of the given provider, or {@code null} if the name is unknown. * If the provider declares many names, the longest name is selected. If many names * have the same length, the one having at largest number of upper-case characters is * selected. This allows this method to return {@code "PNG"} instead than {@code "png"}. * <p> * If no format name has been found, then this method fallback on the shortest MIME type. * Note that the use of shortest MIME type is the opposite of the longest name, but this * is done that way in order to prefer {@code "image/png"} rather than {@code "image/x-png"} * for example. * * @param provider The provider for which we want the name, or {@code null}. * @return The name of the given provider, or {@code null} if none. * * @since 3.07 */ public static String getDisplayName(final ImageReaderWriterSpi provider) { String name = null; if (provider != null) { String[] formats = provider.getFormatNames(); if (formats != null) { for (final String candidate : formats) { int d = candidate.length(); if (d != 0) { if (name != null) { d -= name.length(); } if (d >= 0) { if (d == 0) { int na=0, nb=0; for (int i=candidate.length(); --i>=0;) { if (Character.isUpperCase(candidate.charAt(i))) na++; if (Character.isUpperCase(name .charAt(i))) nb++; } if (na <= nb) { continue; } } name = candidate; } } } } /* * If no format has been found, fallback on MIME types. */ if (name == null) { formats = provider.getMIMETypes(); if (formats != null) { for (final String candidate : formats) { final int length = candidate.length(); if (length != 0 && (name == null || length < name.length())) { name = candidate; } } } } } return name; } /** * Simplifies the given array of format names, MIME types or file suffixes. * This method sorts the elements by alphabetical order, ignoring cases, * and remove duplicated values. * * @param choices The array to simplify. * @return The simplified array. * * @since 3.10 */ public static String[] simplify(String... choices) { if (choices != null) { Arrays.sort(choices, String.CASE_INSENSITIVE_ORDER); int count = 0; for (int i=1; i<choices.length; i++) { final String o1 = choices[i-1]; final String o2 = choices[i]; if (!o1.equalsIgnoreCase(o2)) { choices[count++] = o1; } else if (o1.compareTo(o2) > 0) { choices[i-1] = o2; // Order lower-cases before upper-cases. choices[i] = o1; } } choices = ArraysExt.resize(choices, count); } return choices; } /** * Formats a description from the information provided in the given provider. * * @param spi The provider from which to extract the information. * @param locale The locale to use for localizing the description. * @param appendTo The buffer where to append the description. * * @since 3.15 */ public static void formatDescription(final IIOServiceProvider spi, final Locale locale, final StringBuilder appendTo) { final Vocabulary resources = Vocabulary.getResources(locale); String text = spi.getDescription(locale); if (text == null) { text = resources.getString(Vocabulary.Keys.Unknown); } appendTo.append(text); text = spi.getVersion(); if (text != null) { appendTo.append(" (").append(resources.getString(Vocabulary.Keys.Version_1, text)); text = spi.getVendorName(); if (text != null) { appendTo.append(", ").append(text); } appendTo.append(')'); } } /** * Wraps the given input in an {@link ImageInputStream}, given preference to uncached streams * if possible. It may be faster when reading small images or when reading just the first few * bytes (for example in order to determine if a file is in a known format). * <p> * If no uncached stream can be created, this method fallbacks on the default cached stream. * * @param input The input for which we want an image input stream. * @return The image input stream, or {@code null} if no suitable stream were found. * @throws IOException If an error occurred while creating the stream. * * @since 3.07 */ public static ImageInputStream createUncachedImageInputStream(final Object input) throws IOException { ImageInputStreamSpi fallback = null; final Iterator<ImageInputStreamSpi> it = orderForClassLoader( IIORegistry.getDefaultInstance().getServiceProviders(ImageInputStreamSpi.class, true)); while (it.hasNext()) { final ImageInputStreamSpi spi = it.next(); if (spi.getInputClass().isInstance(input)) { if (!spi.needsCacheFile()) { return spi.createInputStreamInstance(input, false, ImageIO.getCacheDirectory()); } if (fallback == null) { fallback = spi; } } } if (fallback != null) { return fallback.createInputStreamInstance(input, false, ImageIO.getCacheDirectory()); } return null; } /** * Returns an iterator giving precedence to classes loaded by the Geotk class loaderĀ or one * of its children. */ private static <T> Iterator<T> orderForClassLoader(final Iterator<T> iterator) { return Factories.orderForClassLoader(Formats.class.getClassLoader(), iterator); } }