/*
 * Copyright (c) 2000, 2022, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code 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 General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package javax.imageio;

import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.io.File;
import java.io.FilePermission;
import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.net.URL;
import java.security.AccessController;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import javax.imageio.spi.IIORegistry;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.spi.ImageReaderWriterSpi;
import javax.imageio.spi.ImageWriterSpi;
import javax.imageio.spi.ImageInputStreamSpi;
import javax.imageio.spi.ImageOutputStreamSpi;
import javax.imageio.spi.ImageTranscoderSpi;
import javax.imageio.spi.ServiceRegistry;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.ImageOutputStream;
import sun.awt.AppContext;
import sun.security.action.GetPropertyAction;

/**
 * A class containing static convenience methods for locating
 * {@code ImageReader}s and {@code ImageWriter}s, and
 * performing simple encoding and decoding.
 *
 */
public final class ImageIO {

    private static final IIORegistry theRegistry =
        IIORegistry.getDefaultInstance();

    /**
     * Constructor is private to prevent instantiation.
     */
    private ImageIO() {}

    /**
     * Scans for plug-ins on the application class path,
     * loads their service provider classes, and registers a service
     * provider instance for each one found with the
     * {@code IIORegistry}.
     *
     * <p>This method is needed because the application class path can
     * theoretically change, or additional plug-ins may become available.
     * Rather than re-scanning the classpath on every invocation of the
     * API, the class path is scanned automatically only on the first
     * invocation. Clients can call this method to prompt a re-scan.
     * Thus this method need only be invoked by sophisticated applications
     * which dynamically make new plug-ins available at runtime.
     *
     * <p> The {@code getResources} method of the context
     * {@code ClassLoader} is used locate JAR files containing
     * files named
     * {@code META-INF/services/javax.imageio.spi.}<i>classname</i>,
     * where <i>classname</i> is one of {@code ImageReaderSpi},
     * {@code ImageWriterSpi}, {@code ImageTranscoderSpi},
     * {@code ImageInputStreamSpi}, or
     * {@code ImageOutputStreamSpi}, along the application class
     * path.
     *
     * <p> The contents of the located files indicate the names of
     * actual implementation classes which implement the
     * aforementioned service provider interfaces; the default class
     * loader is then used to load each of these classes and to
     * instantiate an instance of each class, which is then placed
     * into the registry for later retrieval.
     *
     * <p> The exact set of locations searched depends on the
     * implementation of the Java runtime environment.
     *
     * @see ClassLoader#getResources
     */
    public static void scanForPlugins() {
        theRegistry.registerApplicationClasspathSpis();
    }

    // ImageInputStreams

    /**
     * A class to hold information about caching.  Each
     * {@code ThreadGroup} will have its own copy
     * via the {@code AppContext} mechanism.
     */
    static class CacheInfo {
        boolean useCache = true;
        File cacheDirectory = null;
        Boolean hasPermission = null;

        public CacheInfo() {}

        public boolean getUseCache() {
            return useCache;
        }

        public void setUseCache(boolean useCache) {
            this.useCache = useCache;
        }

        public File getCacheDirectory() {
            return cacheDirectory;
        }

        public void setCacheDirectory(File cacheDirectory) {
            this.cacheDirectory = cacheDirectory;
        }

        public Boolean getHasPermission() {
            return hasPermission;
        }

        public void setHasPermission(Boolean hasPermission) {
            this.hasPermission = hasPermission;
        }
    }

    /**
     * Returns the {@code CacheInfo} object associated with this
     * {@code ThreadGroup}.
     */
    private static synchronized CacheInfo getCacheInfo() {
        AppContext context = AppContext.getAppContext();
        CacheInfo info = (CacheInfo)context.get(CacheInfo.class);
        if (info == null) {
            info = new CacheInfo();
            context.put(CacheInfo.class, info);
        }
        return info;
    }

    /**
     * Returns the default temporary (cache) directory as defined by the
     * java.io.tmpdir system property.
     */
    @SuppressWarnings("removal")
    private static String getTempDir() {
        GetPropertyAction a = new GetPropertyAction("java.io.tmpdir");
        return AccessController.doPrivileged(a);
    }

    /**
     * Determines whether the caller has write access to the cache
     * directory, stores the result in the {@code CacheInfo} object,
     * and returns the decision.  This method helps to prevent mysterious
     * SecurityExceptions to be thrown when this convenience class is used
     * in an applet, for example.
     */
    private static boolean hasCachePermission() {
        Boolean hasPermission = getCacheInfo().getHasPermission();

        if (hasPermission != null) {
            return hasPermission.booleanValue();
        } else {
            try {
                @SuppressWarnings("removal")
                SecurityManager security = System.getSecurityManager();
                if (security != null) {
                    File cachedir = getCacheDirectory();
                    String cachepath;

                    if (cachedir != null) {
                        cachepath = cachedir.getPath();
                    } else {
                        cachepath = getTempDir();

                        if (cachepath == null || cachepath.isEmpty()) {
                            getCacheInfo().setHasPermission(Boolean.FALSE);
                            return false;
                        }
                    }

                    // we have to check whether we can read, write,
                    // and delete cache files.
                    // So, compose cache file path and check it.
                    String filepath = cachepath;
                    if (!filepath.endsWith(File.separator)) {
                        filepath += File.separator;
                    }
                    filepath += "*";

                    security.checkPermission(new FilePermission(filepath, "read, write, delete"));
                }
            } catch (SecurityException e) {
                getCacheInfo().setHasPermission(Boolean.FALSE);
                return false;
            }

            getCacheInfo().setHasPermission(Boolean.TRUE);
            return true;
        }
    }

    /**
     * Sets a flag indicating whether a disk-based cache file should
     * be used when creating {@code ImageInputStream}s and
     * {@code ImageOutputStream}s.
     *
     * <p> When reading from a standard {@code InputStream}, it
     * may be necessary to save previously read information in a cache
     * since the underlying stream does not allow data to be re-read.
     * Similarly, when writing to a standard
     * {@code OutputStream}, a cache may be used to allow a
     * previously written value to be changed before flushing it to
     * the final destination.
     *
     * <p> The cache may reside in main memory or on disk.  Setting
     * this flag to {@code false} disallows the use of disk for
     * future streams, which may be advantageous when working with
     * small images, as the overhead of creating and destroying files
     * is removed.
     *
     * <p> On startup, the value is set to {@code true}.
     *
     * @param useCache a {@code boolean} indicating whether a
     * cache file should be used, in cases where it is optional.
     *
     * @see #getUseCache
     */
    public static void setUseCache(boolean useCache) {
        getCacheInfo().setUseCache(useCache);
    }

    /**
     * Returns the current value set by {@code setUseCache}, or
     * {@code true} if no explicit setting has been made.
     *
     * @return true if a disk-based cache may be used for
     * {@code ImageInputStream}s and
     * {@code ImageOutputStream}s.
     *
     * @see #setUseCache
     */
    public static boolean getUseCache() {
        return getCacheInfo().getUseCache();
    }

    /**
     * Sets the directory where cache files are to be created.  A
     * value of {@code null} indicates that the system-dependent
     * default temporary-file directory is to be used.  If
     * {@code getUseCache} returns false, this value is ignored.
     *
     * @param cacheDirectory a {@code File} specifying a directory.
     *
     * @see File#createTempFile(String, String, File)
     *
     * @throws SecurityException if the security manager denies
     * access to the directory.
     * @throws IllegalArgumentException if {@code cacheDir} is
     * non-{@code null} but is not a directory.
     *
     * @see #getCacheDirectory
     */
    public static void setCacheDirectory(File cacheDirectory) {
        if ((cacheDirectory != null) && !(cacheDirectory.isDirectory())) {
            throw new IllegalArgumentException("Not a directory!");
        }
        getCacheInfo().setCacheDirectory(cacheDirectory);
        getCacheInfo().setHasPermission(null);
    }

    /**
     * Returns the current value set by
     * {@code setCacheDirectory}, or {@code null} if no
     * explicit setting has been made.
     *
     * @return a {@code File} indicating the directory where
     * cache files will be created, or {@code null} to indicate
     * the system-dependent default temporary-file directory.
     *
     * @see #setCacheDirectory
     */
    public static File getCacheDirectory() {
        return getCacheInfo().getCacheDirectory();
    }

    /**
     * Returns an {@code ImageInputStream} that will take its
     * input from the given {@code Object}.  The set of
     * {@code ImageInputStreamSpi}s registered with the
     * {@code IIORegistry} class is queried and the first one
     * that is able to take input from the supplied object is used to
     * create the returned {@code ImageInputStream}.  If no
     * suitable {@code ImageInputStreamSpi} exists,
     * {@code null} is returned.
     *
     * <p> The current cache settings from {@code getUseCache} and
     * {@code getCacheDirectory} will be used to control caching.
     *
     * @param input an {@code Object} to be used as an input
     * source, such as a {@code File}, readable
     * {@code RandomAccessFile}, or {@code InputStream}.
     *
     * @return an {@code ImageInputStream}, or {@code null}.
     *
     * @throws IllegalArgumentException if {@code input}
     * is {@code null}.
     * @throws IOException if a cache file is needed but cannot be
     * created.
     *
     * @see javax.imageio.spi.ImageInputStreamSpi
     */
    public static ImageInputStream createImageInputStream(Object input)
        throws IOException {
        if (input == null) {
            throw new IllegalArgumentException("input == null!");
        }

        Iterator<ImageInputStreamSpi> iter;
        // Ensure category is present
        try {
            iter = theRegistry.getServiceProviders(ImageInputStreamSpi.class,
                                                   true);
        } catch (IllegalArgumentException e) {
            return null;
        }

        boolean usecache = getUseCache() && hasCachePermission();

        while (iter.hasNext()) {
            ImageInputStreamSpi spi = iter.next();
            if (spi.getInputClass().isInstance(input)) {
                try {
                    return spi.createInputStreamInstance(input,
                                                         usecache,
                                                         getCacheDirectory());
                } catch (IOException e) {
                    throw new IIOException("Can't create cache file!", e);
                }
            }
        }

        return null;
    }

    // ImageOutputStreams

    /**
     * Returns an {@code ImageOutputStream} that will send its
     * output to the given {@code Object}.  The set of
     * {@code ImageOutputStreamSpi}s registered with the
     * {@code IIORegistry} class is queried and the first one
     * that is able to send output from the supplied object is used to
     * create the returned {@code ImageOutputStream}.  If no
     * suitable {@code ImageOutputStreamSpi} exists,
     * {@code null} is returned.
     *
     * <p> The current cache settings from {@code getUseCache} and
     * {@code getCacheDirectory} will be used to control caching.
     *
     * @param output an {@code Object} to be used as an output
     * destination, such as a {@code File}, writable
     * {@code RandomAccessFile}, or {@code OutputStream}.
     *
     * @return an {@code ImageOutputStream}, or
     * {@code null}.
     *
     * @throws IllegalArgumentException if {@code output} is
     * {@code null}.
     * @throws IOException if a cache file is needed but cannot be
     * created.
     *
     * @see javax.imageio.spi.ImageOutputStreamSpi
     */
    public static ImageOutputStream createImageOutputStream(Object output)
        throws IOException {
        if (output == null) {
            throw new IllegalArgumentException("output == null!");
        }

        Iterator<ImageOutputStreamSpi> iter;
        // Ensure category is present
        try {
            iter = theRegistry.getServiceProviders(ImageOutputStreamSpi.class,
                                                   true);
        } catch (IllegalArgumentException e) {
            return null;
        }

        boolean usecache = getUseCache() && hasCachePermission();

        while (iter.hasNext()) {
            ImageOutputStreamSpi spi = iter.next();
            if (spi.getOutputClass().isInstance(output)) {
                try {
                    return spi.createOutputStreamInstance(output,
                                                          usecache,
                                                          getCacheDirectory());
                } catch (IOException e) {
                    throw new IIOException("Can't create cache file!", e);
                }
            }
        }

        return null;
    }

    private static enum SpiInfo {
        FORMAT_NAMES {
            @Override
            String[] info(ImageReaderWriterSpi spi) {
                return spi.getFormatNames();
            }
        },
        MIME_TYPES {
            @Override
            String[] info(ImageReaderWriterSpi spi) {
                return spi.getMIMETypes();
            }
        },
        FILE_SUFFIXES {
            @Override
            String[] info(ImageReaderWriterSpi spi) {
                return spi.getFileSuffixes();
            }
        };

        abstract String[] info(ImageReaderWriterSpi spi);
    }

    private static <S extends ImageReaderWriterSpi>
        String[] getReaderWriterInfo(Class<S> spiClass, SpiInfo spiInfo)
    {
        // Ensure category is present
        Iterator<S> iter;
        try {
            iter = theRegistry.getServiceProviders(spiClass, true);
        } catch (IllegalArgumentException e) {
            return new String[0];
        }

        HashSet<String> s = new HashSet<>();
        while (iter.hasNext()) {
            ImageReaderWriterSpi spi = iter.next();
            String[] info = spiInfo.info(spi);
            if (info != null) {
                Collections.addAll(s, info);
            }
        }

        return s.toArray(new String[s.size()]);
    }

    // Readers

    /**
     * Returns an array of {@code String}s listing all of the
     * informal format names understood by the current set of registered
     * readers.
     *
     * @return an array of {@code String}s.
     */
    public static String[] getReaderFormatNames() {
        return getReaderWriterInfo(ImageReaderSpi.class,
                                   SpiInfo.FORMAT_NAMES);
    }

    /**
     * Returns an array of {@code String}s listing all of the
     * MIME types understood by the current set of registered
     * readers.
     *
     * @return an array of {@code String}s.
     */
    public static String[] getReaderMIMETypes() {
        return getReaderWriterInfo(ImageReaderSpi.class,
                                   SpiInfo.MIME_TYPES);
    }

    /**
     * Returns an array of {@code String}s listing all of the
     * file suffixes associated with the formats understood
     * by the current set of registered readers.
     *
     * @return an array of {@code String}s.
     * @since 1.6
     */
    public static String[] getReaderFileSuffixes() {
        return getReaderWriterInfo(ImageReaderSpi.class,
                                   SpiInfo.FILE_SUFFIXES);
    }

    static class ImageReaderIterator implements Iterator<ImageReader> {
        // Contains ImageReaderSpis
        private Iterator<ImageReaderSpi> iter;

        public ImageReaderIterator(Iterator<ImageReaderSpi> iter) {
            this.iter = iter;
        }

        public boolean hasNext() {
            return iter.hasNext();
        }

        public ImageReader next() {
            ImageReaderSpi spi = null;
            try {
                spi = iter.next();
                return spi.createReaderInstance();
            } catch (IOException e) {
                // Deregister the spi in this case, but only as
                // an ImageReaderSpi
                theRegistry.deregisterServiceProvider(spi, ImageReaderSpi.class);
            }
            return null;
        }

        public void remove() {
            throw new UnsupportedOperationException();
        }
    }

    static class CanDecodeInputFilter
        implements ServiceRegistry.Filter {

        Object input;

        public CanDecodeInputFilter(Object input) {
            this.input = input;
        }

        public boolean filter(Object elt) {
            try {
                ImageReaderSpi spi = (ImageReaderSpi)elt;
                ImageInputStream stream = null;
                if (input instanceof ImageInputStream) {
                    stream = (ImageInputStream)input;
                }

                // Perform mark/reset as a defensive measure
                // even though plug-ins are supposed to take
                // care of it.
                boolean canDecode = false;
                if (stream != null) {
                    stream.mark();
                }
                try {
                    canDecode = spi.canDecodeInput(input);
                } finally {
                    if (stream != null) {
                        stream.reset();
                    }
                }

                return canDecode;
            } catch (IOException e) {
                return false;
            }
        }
    }

    static class CanEncodeImageAndFormatFilter
        implements ServiceRegistry.Filter {

        ImageTypeSpecifier type;
        String formatName;

        public CanEncodeImageAndFormatFilter(ImageTypeSpecifier type,
                                             String formatName) {
            this.type = type;
            this.formatName = formatName;
        }

        public boolean filter(Object elt) {
            ImageWriterSpi spi = (ImageWriterSpi)elt;
            return Arrays.asList(spi.getFormatNames()).contains(formatName) &&
                spi.canEncodeImage(type);
        }
    }

    static class ContainsFilter
        implements ServiceRegistry.Filter {

        Method method;
        String name;

        // method returns an array of Strings
        public ContainsFilter(Method method,
                              String name) {
            this.method = method;
            this.name = name;
        }

        public boolean filter(Object elt) {
            try {
                return contains((String[])method.invoke(elt), name);
            } catch (Exception e) {
                return false;
            }
        }
    }

    /**
     * Returns an {@code Iterator} containing all currently
     * registered {@code ImageReader}s that claim to be able to
     * decode the supplied {@code Object}, typically an
     * {@code ImageInputStream}.
     *
     * <p> The stream position is left at its prior position upon
     * exit from this method.
     *
     * @param input an {@code ImageInputStream} or other
     * {@code Object} containing encoded image data.
     *
     * @return an {@code Iterator} containing {@code ImageReader}s.
     *
     * @throws IllegalArgumentException if {@code input} is
     * {@code null}.
     *
     * @see javax.imageio.spi.ImageReaderSpi#canDecodeInput
     */
    public static Iterator<ImageReader> getImageReaders(Object input) {
        if (input == null) {
            throw new IllegalArgumentException("input == null!");
        }
        Iterator<ImageReaderSpi> iter;
        // Ensure category is present
        try {
            iter = theRegistry.getServiceProviders(ImageReaderSpi.class,
                                              new CanDecodeInputFilter(input),
                                              true);
        } catch (IllegalArgumentException e) {
            return Collections.emptyIterator();
        }

        return new ImageReaderIterator(iter);
    }

    private static Method readerFormatNamesMethod;
    private static Method readerFileSuffixesMethod;
    private static Method readerMIMETypesMethod;
    private static Method writerFormatNamesMethod;
    private static Method writerFileSuffixesMethod;
    private static Method writerMIMETypesMethod;

    static {
        try {
            readerFormatNamesMethod =
                ImageReaderSpi.class.getMethod("getFormatNames");
            readerFileSuffixesMethod =
                ImageReaderSpi.class.getMethod("getFileSuffixes");
            readerMIMETypesMethod =
                ImageReaderSpi.class.getMethod("getMIMETypes");

            writerFormatNamesMethod =
                ImageWriterSpi.class.getMethod("getFormatNames");
            writerFileSuffixesMethod =
                ImageWriterSpi.class.getMethod("getFileSuffixes");
            writerMIMETypesMethod =
                ImageWriterSpi.class.getMethod("getMIMETypes");
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
    }

    /**
     * Returns an {@code Iterator} containing all currently
     * registered {@code ImageReader}s that claim to be able to
     * decode the named format.
     *
     * @param formatName a {@code String} containing the informal
     * name of a format (<i>e.g.</i>, "jpeg" or "tiff".
     *
     * @return an {@code Iterator} containing
     * {@code ImageReader}s.
     *
     * @throws IllegalArgumentException if {@code formatName}
     * is {@code null}.
     *
     * @see javax.imageio.spi.ImageReaderSpi#getFormatNames
     */
    public static Iterator<ImageReader>
        getImageReadersByFormatName(String formatName)
    {
        if (formatName == null) {
            throw new IllegalArgumentException("formatName == null!");
        }
        Iterator<ImageReaderSpi> iter;
        // Ensure category is present
        try {
            iter = theRegistry.getServiceProviders(ImageReaderSpi.class,
                                    new ContainsFilter(readerFormatNamesMethod,
                                                       formatName),
                                                true);
        } catch (IllegalArgumentException e) {
            return Collections.emptyIterator();
        }
        return new ImageReaderIterator(iter);
    }

    /**
     * Returns an {@code Iterator} containing all currently
     * registered {@code ImageReader}s that claim to be able to
     * decode files with the given suffix.
     *
     * @param fileSuffix a {@code String} containing a file
     * suffix (<i>e.g.</i>, "jpg" or "tiff").
     *
     * @return an {@code Iterator} containing
     * {@code ImageReader}s.
     *
     * @throws IllegalArgumentException if {@code fileSuffix}
     * is {@code null}.
     *
     * @see javax.imageio.spi.ImageReaderSpi#getFileSuffixes
     */
    public static Iterator<ImageReader>
        getImageReadersBySuffix(String fileSuffix)
    {
        if (fileSuffix == null) {
            throw new IllegalArgumentException("fileSuffix == null!");
        }
        // Ensure category is present
        Iterator<ImageReaderSpi> iter;
        try {
            iter = theRegistry.getServiceProviders(ImageReaderSpi.class,
                                   new ContainsFilter(readerFileSuffixesMethod,
                                                      fileSuffix),
                                              true);
        } catch (IllegalArgumentException e) {
            return Collections.emptyIterator();
        }
        return new ImageReaderIterator(iter);
    }

    /**
     * Returns an {@code Iterator} containing all currently
     * registered {@code ImageReader}s that claim to be able to
     * decode files with the given MIME type.
     *
     * @param MIMEType a {@code String} containing a file
     * suffix (<i>e.g.</i>, "image/jpeg" or "image/x-bmp").
     *
     * @return an {@code Iterator} containing
     * {@code ImageReader}s.
     *
     * @throws IllegalArgumentException if {@code MIMEType} is
     * {@code null}.
     *
     * @see javax.imageio.spi.ImageReaderSpi#getMIMETypes
     */
    public static Iterator<ImageReader>
        getImageReadersByMIMEType(String MIMEType)
    {
        if (MIMEType == null) {
            throw new IllegalArgumentException("MIMEType == null!");
        }
        // Ensure category is present
        Iterator<ImageReaderSpi> iter;
        try {
            iter = theRegistry.getServiceProviders(ImageReaderSpi.class,
                                      new ContainsFilter(readerMIMETypesMethod,
                                                         MIMEType),
                                              true);
        } catch (IllegalArgumentException e) {
            return Collections.emptyIterator();
        }
        return new ImageReaderIterator(iter);
    }

    // Writers

    /**
     * Returns an array of {@code String}s listing all of the
     * informal format names understood by the current set of registered
     * writers.
     *
     * @return an array of {@code String}s.
     */
    public static String[] getWriterFormatNames() {
        return getReaderWriterInfo(ImageWriterSpi.class,
                                   SpiInfo.FORMAT_NAMES);
    }

    /**
     * Returns an array of {@code String}s listing all of the
     * MIME types understood by the current set of registered
     * writers.
     *
     * @return an array of {@code String}s.
     */
    public static String[] getWriterMIMETypes() {
        return getReaderWriterInfo(ImageWriterSpi.class,
                                   SpiInfo.MIME_TYPES);
    }

    /**
     * Returns an array of {@code String}s listing all of the
     * file suffixes associated with the formats understood
     * by the current set of registered writers.
     *
     * @return an array of {@code String}s.
     * @since 1.6
     */
    public static String[] getWriterFileSuffixes() {
        return getReaderWriterInfo(ImageWriterSpi.class,
                                   SpiInfo.FILE_SUFFIXES);
    }

    static class ImageWriterIterator implements Iterator<ImageWriter> {
        // Contains ImageWriterSpis
        private Iterator<ImageWriterSpi> iter;

        public ImageWriterIterator(Iterator<ImageWriterSpi> iter) {
            this.iter = iter;
        }

        public boolean hasNext() {
            return iter.hasNext();
        }

        public ImageWriter next() {
            ImageWriterSpi spi = null;
            try {
                spi = iter.next();
                return spi.createWriterInstance();
            } catch (IOException e) {
                // Deregister the spi in this case, but only as a writerSpi
                theRegistry.deregisterServiceProvider(spi, ImageWriterSpi.class);
            }
            return null;
        }

        public void remove() {
            throw new UnsupportedOperationException();
        }
    }

    private static boolean contains(String[] names, String name) {
        for (int i = 0; i < names.length; i++) {
            if (name.equalsIgnoreCase(names[i])) {
                return true;
            }
        }

        return false;
    }

    /**
     * Returns an {@code Iterator} containing all currently
     * registered {@code ImageWriter}s that claim to be able to
     * encode the named format.
     *
     * @param formatName a {@code String} containing the informal
     * name of a format (<i>e.g.</i>, "jpeg" or "tiff".
     *
     * @return an {@code Iterator} containing
     * {@code ImageWriter}s.
     *
     * @throws IllegalArgumentException if {@code formatName} is
     * {@code null}.
     *
     * @see javax.imageio.spi.ImageWriterSpi#getFormatNames
     */
    public static Iterator<ImageWriter>
        getImageWritersByFormatName(String formatName)
    {
        if (formatName == null) {
            throw new IllegalArgumentException("formatName == null!");
        }
        Iterator<ImageWriterSpi> iter;
        // Ensure category is present
        try {
            iter = theRegistry.getServiceProviders(ImageWriterSpi.class,
                                    new ContainsFilter(writerFormatNamesMethod,
                                                       formatName),
                                            true);
        } catch (IllegalArgumentException e) {
            return Collections.emptyIterator();
        }
        return new ImageWriterIterator(iter);
    }

    /**
     * Returns an {@code Iterator} containing all currently
     * registered {@code ImageWriter}s that claim to be able to
     * encode files with the given suffix.
     *
     * @param fileSuffix a {@code String} containing a file
     * suffix (<i>e.g.</i>, "jpg" or "tiff").
     *
     * @return an {@code Iterator} containing {@code ImageWriter}s.
     *
     * @throws IllegalArgumentException if {@code fileSuffix} is
     * {@code null}.
     *
     * @see javax.imageio.spi.ImageWriterSpi#getFileSuffixes
     */
    public static Iterator<ImageWriter>
        getImageWritersBySuffix(String fileSuffix)
    {
        if (fileSuffix == null) {
            throw new IllegalArgumentException("fileSuffix == null!");
        }
        Iterator<ImageWriterSpi> iter;
        // Ensure category is present
        try {
            iter = theRegistry.getServiceProviders(ImageWriterSpi.class,
                                   new ContainsFilter(writerFileSuffixesMethod,
                                                      fileSuffix),
                                            true);
        } catch (IllegalArgumentException e) {
            return Collections.emptyIterator();
        }
        return new ImageWriterIterator(iter);
    }

    /**
     * Returns an {@code Iterator} containing all currently
     * registered {@code ImageWriter}s that claim to be able to
     * encode files with the given MIME type.
     *
     * @param MIMEType a {@code String} containing a file
     * suffix (<i>e.g.</i>, "image/jpeg" or "image/x-bmp").
     *
     * @return an {@code Iterator} containing {@code ImageWriter}s.
     *
     * @throws IllegalArgumentException if {@code MIMEType} is
     * {@code null}.
     *
     * @see javax.imageio.spi.ImageWriterSpi#getMIMETypes
     */
    public static Iterator<ImageWriter>
        getImageWritersByMIMEType(String MIMEType)
    {
        if (MIMEType == null) {
            throw new IllegalArgumentException("MIMEType == null!");
        }
        Iterator<ImageWriterSpi> iter;
        // Ensure category is present
        try {
            iter = theRegistry.getServiceProviders(ImageWriterSpi.class,
                                      new ContainsFilter(writerMIMETypesMethod,
                                                         MIMEType),
                                            true);
        } catch (IllegalArgumentException e) {
            return Collections.emptyIterator();
        }
        return new ImageWriterIterator(iter);
    }

    /**
     * Returns an {@code ImageWriter} corresponding to the given
     * {@code ImageReader}, if there is one, or {@code null}
     * if the plug-in for this {@code ImageReader} does not
     * specify a corresponding {@code ImageWriter}, or if the
     * given {@code ImageReader} is not registered.  This
     * mechanism may be used to obtain an {@code ImageWriter}
     * that will understand the internal structure of non-pixel
     * metadata (as encoded by {@code IIOMetadata} objects)
     * generated by the {@code ImageReader}.  By obtaining this
     * data from the {@code ImageReader} and passing it on to the
     * {@code ImageWriter} obtained with this method, a client
     * program can read an image, modify it in some way, and write it
     * back out preserving all metadata, without having to understand
     * anything about the structure of the metadata, or even about
     * the image format.  Note that this method returns the
     * "preferred" writer, which is the first in the list returned by
     * {@code javax.imageio.spi.ImageReaderSpi.getImageWriterSpiNames()}.
     *
     * @param reader an instance of a registered {@code ImageReader}.
     *
     * @return an {@code ImageWriter}, or null.
     *
     * @throws IllegalArgumentException if {@code reader} is
     * {@code null}.
     *
     * @see #getImageReader(ImageWriter)
     * @see javax.imageio.spi.ImageReaderSpi#getImageWriterSpiNames()
     */
    public static ImageWriter getImageWriter(ImageReader reader) {
        if (reader == null) {
            throw new IllegalArgumentException("reader == null!");
        }

        ImageReaderSpi readerSpi = reader.getOriginatingProvider();
        if (readerSpi == null) {
            Iterator<ImageReaderSpi> readerSpiIter;
            // Ensure category is present
            try {
                readerSpiIter =
                    theRegistry.getServiceProviders(ImageReaderSpi.class,
                                                    false);
            } catch (IllegalArgumentException e) {
                return null;
            }

            while (readerSpiIter.hasNext()) {
                ImageReaderSpi temp = readerSpiIter.next();
                if (temp.isOwnReader(reader)) {
                    readerSpi = temp;
                    break;
                }
            }
            if (readerSpi == null) {
                return null;
            }
        }

        String[] writerNames = readerSpi.getImageWriterSpiNames();
        if (writerNames == null) {
            return null;
        }

        Class<?> writerSpiClass = null;
        try {
            writerSpiClass = Class.forName(writerNames[0], true,
                                           ClassLoader.getSystemClassLoader());
        } catch (ClassNotFoundException e) {
            return null;
        }

        ImageWriterSpi writerSpi = (ImageWriterSpi)
            theRegistry.getServiceProviderByClass(writerSpiClass);
        if (writerSpi == null) {
            return null;
        }

        try {
            return writerSpi.createWriterInstance();
        } catch (IOException e) {
            // Deregister the spi in this case, but only as a writerSpi
            theRegistry.deregisterServiceProvider(writerSpi,
                                                  ImageWriterSpi.class);
            return null;
        }
    }

    /**
     * Returns an {@code ImageReader} corresponding to the given
     * {@code ImageWriter}, if there is one, or {@code null}
     * if the plug-in for this {@code ImageWriter} does not
     * specify a corresponding {@code ImageReader}, or if the
     * given {@code ImageWriter} is not registered.  This method
     * is provided principally for symmetry with
     * {@code getImageWriter(ImageReader)}.  Note that this
     * method returns the "preferred" reader, which is the first in
     * the list returned by
     * javax.imageio.spi.ImageWriterSpi.{@code getImageReaderSpiNames()}.
     *
     * @param writer an instance of a registered {@code ImageWriter}.
     *
     * @return an {@code ImageReader}, or null.
     *
     * @throws IllegalArgumentException if {@code writer} is
     * {@code null}.
     *
     * @see #getImageWriter(ImageReader)
     * @see javax.imageio.spi.ImageWriterSpi#getImageReaderSpiNames()
     */
    public static ImageReader getImageReader(ImageWriter writer) {
        if (writer == null) {
            throw new IllegalArgumentException("writer == null!");
        }

        ImageWriterSpi writerSpi = writer.getOriginatingProvider();
        if (writerSpi == null) {
            Iterator<ImageWriterSpi> writerSpiIter;
            // Ensure category is present
            try {
                writerSpiIter =
                    theRegistry.getServiceProviders(ImageWriterSpi.class,
                                                    false);
            } catch (IllegalArgumentException e) {
                return null;
            }

            while (writerSpiIter.hasNext()) {
                ImageWriterSpi temp = writerSpiIter.next();
                if (temp.isOwnWriter(writer)) {
                    writerSpi = temp;
                    break;
                }
            }
            if (writerSpi == null) {
                return null;
            }
        }

        String[] readerNames = writerSpi.getImageReaderSpiNames();
        if (readerNames == null) {
            return null;
        }

        Class<?> readerSpiClass = null;
        try {
            readerSpiClass = Class.forName(readerNames[0], true,
                                           ClassLoader.getSystemClassLoader());
        } catch (ClassNotFoundException e) {
            return null;
        }

        ImageReaderSpi readerSpi = (ImageReaderSpi)
            theRegistry.getServiceProviderByClass(readerSpiClass);
        if (readerSpi == null) {
            return null;
        }

        try {
            return readerSpi.createReaderInstance();
        } catch (IOException e) {
            // Deregister the spi in this case, but only as a readerSpi
            theRegistry.deregisterServiceProvider(readerSpi,
                                                  ImageReaderSpi.class);
            return null;
        }
    }

    /**
     * Returns an {@code Iterator} containing all currently
     * registered {@code ImageWriter}s that claim to be able to
     * encode images of the given layout (specified using an
     * {@code ImageTypeSpecifier}) in the given format.
     *
     * @param type an {@code ImageTypeSpecifier} indicating the
     * layout of the image to be written.
     * @param formatName the informal name of the {@code format}.
     *
     * @return an {@code Iterator} containing {@code ImageWriter}s.
     *
     * @throws IllegalArgumentException if any parameter is
     * {@code null}.
     *
     * @see javax.imageio.spi.ImageWriterSpi#canEncodeImage(ImageTypeSpecifier)
     */
    public static Iterator<ImageWriter>
        getImageWriters(ImageTypeSpecifier type, String formatName)
    {
        if (type == null) {
            throw new IllegalArgumentException("type == null!");
        }
        if (formatName == null) {
            throw new IllegalArgumentException("formatName == null!");
        }

        Iterator<ImageWriterSpi> iter;
        // Ensure category is present
        try {
            iter = theRegistry.getServiceProviders(ImageWriterSpi.class,
                                 new CanEncodeImageAndFormatFilter(type,
                                                                   formatName),
                                            true);
        } catch (IllegalArgumentException e) {
            return Collections.emptyIterator();
        }

        return new ImageWriterIterator(iter);
    }

    static class ImageTranscoderIterator
        implements Iterator<ImageTranscoder>
    {
        // Contains ImageTranscoderSpis
        public Iterator<ImageTranscoderSpi> iter;

        public ImageTranscoderIterator(Iterator<ImageTranscoderSpi> iter) {
            this.iter = iter;
        }

        public boolean hasNext() {
            return iter.hasNext();
        }

        public ImageTranscoder next() {
            ImageTranscoderSpi spi = null;
            spi = iter.next();
            return spi.createTranscoderInstance();
        }

        public void remove() {
            throw new UnsupportedOperationException();
        }
    }

    static class TranscoderFilter
        implements ServiceRegistry.Filter {

        String readerSpiName;
        String writerSpiName;

        public TranscoderFilter(ImageReaderSpi readerSpi,
                                ImageWriterSpi writerSpi) {
            this.readerSpiName = readerSpi.getClass().getName();
            this.writerSpiName = writerSpi.getClass().getName();
        }

        public boolean filter(Object elt) {
            ImageTranscoderSpi spi = (ImageTranscoderSpi)elt;
            String readerName = spi.getReaderServiceProviderName();
            String writerName = spi.getWriterServiceProviderName();
            return (readerName.equals(readerSpiName) &&
                    writerName.equals(writerSpiName));
        }
    }

    /**
     * Returns an {@code Iterator} containing all currently
     * registered {@code ImageTranscoder}s that claim to be
     * able to transcode between the metadata of the given
     * {@code ImageReader} and {@code ImageWriter}.
     *
     * @param reader an {@code ImageReader}.
     * @param writer an {@code ImageWriter}.
     *
     * @return an {@code Iterator} containing
     * {@code ImageTranscoder}s.
     *
     * @throws IllegalArgumentException if {@code reader} or
     * {@code writer} is {@code null}.
     */
    public static Iterator<ImageTranscoder>
        getImageTranscoders(ImageReader reader, ImageWriter writer)
    {
        if (reader == null) {
            throw new IllegalArgumentException("reader == null!");
        }
        if (writer == null) {
            throw new IllegalArgumentException("writer == null!");
        }
        ImageReaderSpi readerSpi = reader.getOriginatingProvider();
        ImageWriterSpi writerSpi = writer.getOriginatingProvider();
        ServiceRegistry.Filter filter =
            new TranscoderFilter(readerSpi, writerSpi);

        Iterator<ImageTranscoderSpi> iter;
        // Ensure category is present
        try {
            iter = theRegistry.getServiceProviders(ImageTranscoderSpi.class,
                                            filter, true);
        } catch (IllegalArgumentException e) {
            return Collections.emptyIterator();
        }
        return new ImageTranscoderIterator(iter);
    }

    // All-in-one methods

    /**
     * Returns a {@code BufferedImage} as the result of decoding
     * a supplied {@code File} with an {@code ImageReader}
     * chosen automatically from among those currently registered.
     * The {@code File} is wrapped in an
     * {@code ImageInputStream}.  If no registered
     * {@code ImageReader} claims to be able to read the
     * resulting stream, {@code null} is returned.
     *
     * <p> The current cache settings from {@code getUseCache} and
     * {@code getCacheDirectory} will be used to control caching in the
     * {@code ImageInputStream} that is created.
     *
     * <p> Note that there is no {@code read} method that takes a
     * filename as a {@code String}; use this method instead after
     * creating a {@code File} from the filename.
     *
     * <p> This method does not attempt to locate
     * {@code ImageReader}s that can read directly from a
     * {@code File}; that may be accomplished using
     * {@code IIORegistry} and {@code ImageReaderSpi}.
     *
     * @param input a {@code File} to read from.
     *
     * @return a {@code BufferedImage} containing the decoded
     * contents of the input, or {@code null}.
     *
     * @throws IllegalArgumentException if {@code input} is
     * {@code null}.
     * @throws IOException if an error occurs during reading or when not
     * able to create required ImageInputStream.
     */
    public static BufferedImage read(File input) throws IOException {
        if (input == null) {
            throw new IllegalArgumentException("input == null!");
        }
        if (!input.canRead()) {
            throw new IIOException("Can't read input file!");
        }

        ImageInputStream stream = createImageInputStream(input);
        if (stream == null) {
            throw new IIOException("Can't create an ImageInputStream!");
        }
        BufferedImage bi = read(stream);
        if (bi == null) {
            stream.close();
        }
        return bi;
    }

    /**
     * Returns a {@code BufferedImage} as the result of decoding
     * a supplied {@code InputStream} with an {@code ImageReader}
     * chosen automatically from among those currently registered.
     * The {@code InputStream} is wrapped in an
     * {@code ImageInputStream}.  If no registered
     * {@code ImageReader} claims to be able to read the
     * resulting stream, {@code null} is returned.
     *
     * <p> The current cache settings from {@code getUseCache} and
     * {@code getCacheDirectory} will be used to control caching in the
     * {@code ImageInputStream} that is created.
     *
     * <p> This method does not attempt to locate
     * {@code ImageReader}s that can read directly from an
     * {@code InputStream}; that may be accomplished using
     * {@code IIORegistry} and {@code ImageReaderSpi}.
     *
     * <p> This method <em>does not</em> close the provided
     * {@code InputStream} after the read operation has completed;
     * it is the responsibility of the caller to close the stream, if desired.
     *
     * @param input an {@code InputStream} to read from.
     *
     * @return a {@code BufferedImage} containing the decoded
     * contents of the input, or {@code null}.
     *
     * @throws IllegalArgumentException if {@code input} is
     * {@code null}.
     * @throws IOException if an error occurs during reading or when not
     * able to create required ImageInputStream.
     */
    public static BufferedImage read(InputStream input) throws IOException {
        if (input == null) {
            throw new IllegalArgumentException("input == null!");
        }

        ImageInputStream stream = createImageInputStream(input);
        if (stream == null) {
            throw new IIOException("Can't create an ImageInputStream!");
        }
        BufferedImage bi = read(stream);
        if (bi == null) {
            stream.close();
        }
        return bi;
    }

    /**
     * Returns a {@code BufferedImage} as the result of decoding
     * a supplied {@code URL} with an {@code ImageReader}
     * chosen automatically from among those currently registered.  An
     * {@code InputStream} is obtained from the {@code URL},
     * which is wrapped in an {@code ImageInputStream}.  If no
     * registered {@code ImageReader} claims to be able to read
     * the resulting stream, {@code null} is returned.
     *
     * <p> The current cache settings from {@code getUseCache} and
     * {@code getCacheDirectory} will be used to control caching in the
     * {@code ImageInputStream} that is created.
     *
     * <p> This method does not attempt to locate
     * {@code ImageReader}s that can read directly from a
     * {@code URL}; that may be accomplished using
     * {@code IIORegistry} and {@code ImageReaderSpi}.
     *
     * @param input a {@code URL} to read from.
     *
     * @return a {@code BufferedImage} containing the decoded
     * contents of the input, or {@code null}.
     *
     * @throws IllegalArgumentException if {@code input} is
     * {@code null}.
     * @throws IOException if an error occurs during reading or when not
     * able to create required ImageInputStream.
     */
    public static BufferedImage read(URL input) throws IOException {
        if (input == null) {
            throw new IllegalArgumentException("input == null!");
        }

        InputStream istream;
        try {
            istream = input.openStream();
        } catch (IOException e) {
            throw new IIOException("Can't get input stream from URL!", e);
        }
        ImageInputStream stream = createImageInputStream(istream);
        if (stream == null) {
            /* close the istream when stream is null so that if user has
             * given filepath as URL he can delete it, otherwise stream will
             * be open to that file and he will not be able to delete it.
             */
            istream.close();
            throw new IIOException("Can't create an ImageInputStream!");
        }
        BufferedImage bi;
        try (istream) {
            bi = read(stream);
            if (bi == null) {
                stream.close();
            }
        }
        return bi;
    }

    /**
     * Returns a {@code BufferedImage} as the result of decoding
     * a supplied {@code ImageInputStream} with an
     * {@code ImageReader} chosen automatically from among those
     * currently registered.  If no registered
     * {@code ImageReader} claims to be able to read the stream,
     * {@code null} is returned.
     *
     * <p> Unlike most other methods in this class, this method <em>does</em>
     * close the provided {@code ImageInputStream} after the read
     * operation has completed, unless {@code null} is returned,
     * in which case this method <em>does not</em> close the stream.
     *
     * @param stream an {@code ImageInputStream} to read from.
     *
     * @return a {@code BufferedImage} containing the decoded
     * contents of the input, or {@code null}.
     *
     * @throws IllegalArgumentException if {@code stream} is
     * {@code null}.
     * @throws IOException if an error occurs during reading.
     */
    public static BufferedImage read(ImageInputStream stream)
        throws IOException {
        if (stream == null) {
            throw new IllegalArgumentException("stream == null!");
        }

        Iterator<ImageReader> iter = getImageReaders(stream);
        if (!iter.hasNext()) {
            return null;
        }

        ImageReader reader = iter.next();
        ImageReadParam param = reader.getDefaultReadParam();
        reader.setInput(stream, true, true);
        BufferedImage bi;
        try (stream) {
            bi = reader.read(0, param);
        } catch (RuntimeException e) {
            throw new IIOException(e.toString(), e);
        } finally {
            reader.dispose();
        }
        return bi;
    }

    /**
     * Writes an image using an arbitrary {@code ImageWriter}
     * that supports the given format to an
     * {@code ImageOutputStream}.  The image is written to the
     * {@code ImageOutputStream} starting at the current stream
     * pointer, overwriting existing stream data from that point
     * forward, if present.
     *
     * <p> This method <em>does not</em> close the provided
     * {@code ImageOutputStream} after the write operation has completed;
     * it is the responsibility of the caller to close the stream, if desired.
     *
     * @param im a {@code RenderedImage} to be written.
     * @param formatName a {@code String} containing the informal
     * name of the format.
     * @param output an {@code ImageOutputStream} to be written to.
     *
     * @return {@code false} if no appropriate writer is found.
     *
     * @throws IllegalArgumentException if any parameter is
     * {@code null}.
     * @throws IOException if an error occurs during writing.
     */
    public static boolean write(RenderedImage im,
                                String formatName,
                                ImageOutputStream output) throws IOException {
        if (im == null) {
            throw new IllegalArgumentException("im == null!");
        }
        if (formatName == null) {
            throw new IllegalArgumentException("formatName == null!");
        }
        if (output == null) {
            throw new IllegalArgumentException("output == null!");
        }

        return doWrite(im, getWriter(im, formatName), output);
    }

    /**
     * Writes an image using an arbitrary {@code ImageWriter}
     * that supports the given format to a {@code File}.  If
     * there is already a {@code File} present, its contents are
     * discarded.
     *
     * @param im a {@code RenderedImage} to be written.
     * @param formatName a {@code String} containing the informal
     * name of the format.
     * @param output a {@code File} to be written to.
     *
     * @return {@code false} if no appropriate writer is found.
     *
     * @throws IllegalArgumentException if any parameter is
     * {@code null}.
     * @throws IOException if an error occurs during writing or when not
     * able to create required ImageOutputStream.
     */
    public static boolean write(RenderedImage im,
                                String formatName,
                                File output) throws IOException {
        if (output == null) {
            throw new IllegalArgumentException("output == null!");
        }

        ImageWriter writer = getWriter(im, formatName);
        if (writer == null) {
            /* Do not make changes in the file system if we have
             * no appropriate writer.
             */
            return false;
        }

        output.delete();
        ImageOutputStream stream = createImageOutputStream(output);
        if (stream == null) {
            throw new IIOException("Can't create an ImageOutputStream!");
        }
        try (stream) {
            return doWrite(im, writer, stream);
        }
    }

    /**
     * Writes an image using an arbitrary {@code ImageWriter}
     * that supports the given format to an {@code OutputStream}.
     *
     * <p> This method <em>does not</em> close the provided
     * {@code OutputStream} after the write operation has completed;
     * it is the responsibility of the caller to close the stream, if desired.
     *
     * <p> The current cache settings from {@code getUseCache} and
     * {@code getCacheDirectory} will be used to control caching.
     *
     * @param im a {@code RenderedImage} to be written.
     * @param formatName a {@code String} containing the informal
     * name of the format.
     * @param output an {@code OutputStream} to be written to.
     *
     * @return {@code false} if no appropriate writer is found.
     *
     * @throws IllegalArgumentException if any parameter is
     * {@code null}.
     * @throws IOException if an error occurs during writing or when not
     * able to create required ImageOutputStream.
     */
    public static boolean write(RenderedImage im,
                                String formatName,
                                OutputStream output) throws IOException {
        if (output == null) {
            throw new IllegalArgumentException("output == null!");
        }
        ImageOutputStream stream = createImageOutputStream(output);
        if (stream == null) {
            throw new IIOException("Can't create an ImageOutputStream!");
        }
        try (stream) {
            return doWrite(im, getWriter(im, formatName), stream);
        }
    }

    /**
     * Returns {@code ImageWriter} instance according to given
     * rendered image and image format or {@code null} if there
     * is no appropriate writer.
     */
    private static ImageWriter getWriter(RenderedImage im,
                                         String formatName) {
        ImageTypeSpecifier type =
            ImageTypeSpecifier.createFromRenderedImage(im);
        Iterator<ImageWriter> iter = getImageWriters(type, formatName);

        if (iter.hasNext()) {
            return iter.next();
        } else {
            return null;
        }
    }

    /**
     * Writes image to output stream  using given image writer.
     */
    private static boolean doWrite(RenderedImage im, ImageWriter writer,
                                 ImageOutputStream output) throws IOException {
        if (writer == null) {
            return false;
        }
        writer.setOutput(output);
        try {
            writer.write(im);
        } finally {
            writer.dispose();
            output.flush();
        }
        return true;
    }
}
