package gr.iti.mklab.visual.utilities;

/*
 * %W% %E%
 *
 * Copyright (c) 2006, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 */

import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
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.IIOException;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.ImageTranscoder;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.ImageWriter;
import javax.imageio.spi.IIORegistry;
import javax.imageio.spi.ImageInputStreamSpi;
import javax.imageio.spi.ImageOutputStreamSpi;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.spi.ImageReaderWriterSpi;
import javax.imageio.spi.ImageTranscoderSpi;
import javax.imageio.spi.ImageWriterSpi;
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</code>s and
 * <code>ImageWriter</code>s, and performing simple encoding and decoding.
 * 
 * @version 0.5
 */
public final class ImageIOGreyScale {

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

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

	/**
	 * 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</code>.
	 * 
	 * <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</code> method of the context <code>ClassLoader</code> is used locate JAR files
	 * containing files named <code>META-INF/services/javax.imageio.spi.</code><i>classname</i>, where
	 * <i>classname</i> is one of <code>ImageReaderSpi</code>, <code>ImageWriterSpi</code>,
	 * <code>ImageTranscoderSpi</code>, <code>ImageInputStreamSpi</code>, or <code>ImageOutputStreamSpi</code>
	 * , 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 enviroment.
	 * 
	 * @see ClassLoader#getResources
	 */
	public static void scanForPlugins() {
		theRegistry.registerApplicationClasspathSpis();
	}

	// ImageInputStreams

	/**
	 * A class to hold information about caching. Each <code>ThreadGroup</code> will have its own copy via the
	 * <code>AppContext</code> 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</code> object associated with this <code>ThreadGroup</code>.
	 */
	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.
	 */
	private static String getTempDir() {
		GetPropertyAction a = new GetPropertyAction("java.io.tmpdir");
		return (String) AccessController.doPrivileged(a);
	}

	/**
	 * Determines whether the caller has write access to the cache directory, stores the result in the
	 * <code>CacheInfo</code> 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 {
				SecurityManager security = System.getSecurityManager();
				if (security != null) {
					File cachedir = getCacheDirectory();
					String cachepath;

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

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

					security.checkWrite(cachepath);
				}
			} 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</code>s and <code>ImageOutputStream</code>s.
	 * 
	 * <p>
	 * When reading from a standard <code>InputStream</code>>, 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</code>, 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</code> 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</code>.
	 * 
	 * @param useCache
	 *            a <code>boolean</code> 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</code>, or <code>true</code> if no explicit setting
	 * has been made.
	 * 
	 * @return true if a disk-based cache may be used for <code>ImageInputStream</code>s and
	 *         <code>ImageOutputStream</code>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</code> indicates that the
	 * system-dependent default temporary-file directory is to be used. If <code>getUseCache</code> returns
	 * false, this value is ignored.
	 * 
	 * @param cacheDirectory
	 *            a <code>File</code> specifying a directory.
	 * 
	 * @see File#createTempFile(String, String, File)
	 * 
	 * @exception SecurityException
	 *                if the security manager denies access to the directory.
	 * @exception IllegalArgumentException
	 *                if <code>cacheDir</code> is non-<code>null</code> 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</code>, or <code>null</code> if no explicit
	 * setting has been made.
	 * 
	 * @return a <code>File</code> indicating the directory where cache files will be created, or
	 *         <code>null</code> to indicate the system-dependent default temporary-file directory.
	 * 
	 * @see #setCacheDirectory
	 */
	public static File getCacheDirectory() {
		return getCacheInfo().getCacheDirectory();
	}

	/**
	 * Returns an <code>ImageInputStream</code> that will take its input from the given <code>Object</code>.
	 * The set of <code>ImageInputStreamSpi</code>s registered with the <code>IIORegistry</code> 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</code>. If no suitable <code>ImageInputStreamSpi</code> exists,
	 * <code>null</code> is returned.
	 * 
	 * <p>
	 * The current cache settings from <code>getUseCache</code>and <code>getCacheDirectory</code> will be used
	 * to control caching.
	 * 
	 * @param input
	 *            an <code>Object</code> to be used as an input source, such as a <code>File</code>, readable
	 *            <code>RandomAccessFile</code>, or <code>InputStream</code>.
	 * 
	 * @return an <code>ImageInputStream</code>, or <code>null</code>.
	 * 
	 * @exception IllegalArgumentException
	 *                if <code>input</code> is <code>null</code>.
	 * @exception 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 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 = (ImageInputStreamSpi) 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</code> that will send its output to the given <code>Object</code>.
	 * The set of <code>ImageOutputStreamSpi</code>s registered with the <code>IIORegistry</code> 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</code>. If no suitable <code>ImageOutputStreamSpi</code> exists,
	 * <code>null</code> is returned.
	 * 
	 * <p>
	 * The current cache settings from <code>getUseCache</code>and <code>getCacheDirectory</code> will be used
	 * to control caching.
	 * 
	 * @param output
	 *            an <code>Object</code> to be used as an output destination, such as a <code>File</code>,
	 *            writable <code>RandomAccessFile</code>, or <code>OutputStream</code>.
	 * 
	 * @return an <code>ImageOutputStream</code>, or <code>null</code>.
	 * 
	 * @exception IllegalArgumentException
	 *                if <code>output</code> is <code>null</code>.
	 * @exception 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 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 = (ImageOutputStreamSpi) 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<String>();
		while (iter.hasNext()) {
			ImageReaderWriterSpi spi = iter.next();
			Collections.addAll(s, spiInfo.info(spi));
		}

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

	// Readers

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

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

	/**
	 * Returns an array of <code>String</code>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</code>s.
	 * @since 1.6
	 */
	public static String[] getReaderFileSuffixes() {
		return getReaderWriterInfo(ImageReaderSpi.class, SpiInfo.FILE_SUFFIXES);
	}

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

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

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

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

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

	/**
	 * Returns an <code>Iterator</code> containing all currently registered <code>ImageReader</code>s that
	 * claim to be able to decode files with the given suffix.
	 * 
	 * @param fileSuffix
	 *            a <code>String</code> containing a file suffix (<i>e.g.</i>, "jpg" or "tiff").
	 * 
	 * @return an <code>Iterator</code> containing <code>ImageReader</code>s.
	 * 
	 * @exception IllegalArgumentException
	 *                if <code>fileSuffix</code> is <code>null</code>.
	 * 
	 * @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 iter;
		try {
			iter = theRegistry.getServiceProviders(ImageReaderSpi.class, new ContainsFilter(readerFileSuffixesMethod, fileSuffix), true);
		} catch (IllegalArgumentException e) {
			return new HashSet().iterator();
		}
		return new ImageReaderIterator(iter);
	}

	/**
	 * Returns an <code>Iterator</code> containing all currently registered <code>ImageReader</code>s that
	 * claim to be able to decode files with the given MIME type.
	 * 
	 * @param MIMEType
	 *            a <code>String</code> containing a file suffix (<i>e.g.</i>, "image/jpeg" or "image/x-bmp").
	 * 
	 * @return an <code>Iterator</code> containing <code>ImageReader</code>s.
	 * 
	 * @exception IllegalArgumentException
	 *                if <code>MIMEType</code> is <code>null</code>.
	 * 
	 * @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 iter;
		try {
			iter = theRegistry.getServiceProviders(ImageReaderSpi.class, new ContainsFilter(readerMIMETypesMethod, MIMEType), true);
		} catch (IllegalArgumentException e) {
			return new HashSet().iterator();
		}
		return new ImageReaderIterator(iter);
	}

	// Writers

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

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

	/**
	 * Returns an array of <code>String</code>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</code>s.
	 * @since 1.6
	 */
	public static String[] getWriterFileSuffixes() {
		return getReaderWriterInfo(ImageWriterSpi.class, SpiInfo.FILE_SUFFIXES);
	}

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

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

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

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

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

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

	/**
	 * Returns an <code>ImageWriter</code>corresponding to the given <code>ImageReader</code>, if there is
	 * one, or <code>null</code> if the plug-in for this <code>ImageReader</code> does not specify a
	 * corresponding <code>ImageWriter</code>, or if the given <code>ImageReader</code> is not registered.
	 * This mechanism may be used to obtain an <code>ImageWriter</code> that will understand the internal
	 * structure of non-pixel metadata (as encoded by <code>IIOMetadata</code> objects) generated by the
	 * <code>ImageReader</code>. By obtaining this data from the <code>ImageReader</code> and passing it on to
	 * the <code>ImageWriter</code> 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()</code>.
	 * 
	 * @param reader
	 *            an instance of a registered <code>ImageReader</code>.
	 * 
	 * @return an <code>ImageWriter</code>, or null.
	 * 
	 * @exception IllegalArgumentException
	 *                if <code>reader</code> is <code>null</code>.
	 * 
	 * @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 readerSpiIter;
			// Ensure category is present
			try {
				readerSpiIter = theRegistry.getServiceProviders(ImageReaderSpi.class, false);
			} catch (IllegalArgumentException e) {
				return null;
			}

			while (readerSpiIter.hasNext()) {
				ImageReaderSpi temp = (ImageReaderSpi) 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</code>corresponding to the given <code>ImageWriter</code>, if there is
	 * one, or <code>null</code> if the plug-in for this <code>ImageWriter</code> does not specify a
	 * corresponding <code>ImageReader</code>, or if the given <code>ImageWriter</code> is not registered.
	 * This method is provided principally for symmetry with <code>getImageWriter(ImageReader)</code>. Note
	 * that this method returns the "preferred" reader, which is the first in the list returned by
	 * javax.imageio.spi.ImageWriterSpi.<code>getImageReaderSpiNames()</code>.
	 * 
	 * @param writer
	 *            an instance of a registered <code>ImageWriter</code>.
	 * 
	 * @return an <code>ImageReader</code>, or null.
	 * 
	 * @exception IllegalArgumentException
	 *                if <code>writer</code> is <code>null</code>.
	 * 
	 * @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 writerSpiIter;
			// Ensure category is present
			try {
				writerSpiIter = theRegistry.getServiceProviders(ImageWriterSpi.class, false);
			} catch (IllegalArgumentException e) {
				return null;
			}

			while (writerSpiIter.hasNext()) {
				ImageWriterSpi temp = (ImageWriterSpi) 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</code> containing all currently registered <code>ImageWriter</code>s that
	 * claim to be able to encode images of the given layout (specified using an
	 * <code>ImageTypeSpecifier</code>) in the given format.
	 * 
	 * @param type
	 *            an <code>ImageTypeSpecifier</code> indicating the layout of the image to be written.
	 * @param formatName
	 *            the informal name of the <code>format</code>.
	 * 
	 * @return an <code>Iterator</code> containing <code>ImageWriter</code>s.
	 * 
	 * @exception IllegalArgumentException
	 *                if any parameter is <code>null</code>.
	 * 
	 * @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 iter;
		// Ensure category is present
		try {
			iter = theRegistry.getServiceProviders(ImageWriterSpi.class, new CanEncodeImageAndFormatFilter(type, formatName), true);
		} catch (IllegalArgumentException e) {
			return new HashSet().iterator();
		}

		return new ImageWriterIterator(iter);
	}

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

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

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

		public ImageTranscoder next() {
			ImageTranscoderSpi spi = null;
			spi = (ImageTranscoderSpi) 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</code> containing all currently registered <code>ImageTranscoder</code>s that
	 * claim to be able to transcode between the metadata of the given <code>ImageReader</code> and
	 * <code>ImageWriter</code>.
	 * 
	 * @param reader
	 *            an <code>ImageReader</code>.
	 * @param writer
	 *            an <code>ImageWriter</code>.
	 * 
	 * @return an <code>Iterator</code> containing <code>ImageTranscoder</code>s.
	 * 
	 * @exception IllegalArgumentException
	 *                if <code>reader</code> or <code>writer</code> is <code>null</code>.
	 */
	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 iter;
		// Ensure category is present
		try {
			iter = theRegistry.getServiceProviders(ImageTranscoderSpi.class, filter, true);
		} catch (IllegalArgumentException e) {
			return new HashSet().iterator();
		}
		return new ImageTranscoderIterator(iter);
	}

	// All-in-one methods

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

		ImageInputStream stream = createImageInputStream(input);
		BufferedImage bi = read(stream);
		if (bi == null) {
			stream.close();
		}
		return bi;
	}

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

		InputStream istream = null;
		try {
			istream = input.openStream();
		} catch (IOException e) {
			throw new IIOException("Can't get input stream from URL!", e);
		}
		ImageInputStream stream = createImageInputStream(istream);
		BufferedImage bi;
		try {
			bi = read(stream);
			if (bi == null) {
				stream.close();
			}
		} finally {
			istream.close();
		}
		return bi;
	}

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

		BufferedImage bi = null;
		Iterator iter = getImageReaders(stream);

		IOException lastException = null;
		while (iter.hasNext()) {
			ImageReader reader = null;
			try {
				reader = (ImageReader) iter.next();
				ImageReadParam param = reader.getDefaultReadParam();
				reader.setInput(stream, true, true);
				Iterator<ImageTypeSpecifier> imageTypes = reader.getImageTypes(0);
				while (imageTypes.hasNext()) {
					ImageTypeSpecifier imageTypeSpecifier = imageTypes.next();
					int bufferedImageType = imageTypeSpecifier.getBufferedImageType();
					if (bufferedImageType == BufferedImage.TYPE_BYTE_GRAY) {
						param.setDestinationType(imageTypeSpecifier);
						break;
					}
				}
				bi = reader.read(0, param);
				if (null != bi)
					break;
			} catch (IOException e) {
				lastException = e;
			} finally {
				if (null != reader) {
					reader.dispose();
					stream.close();
				}
			}
		}
		// If you don't have an image at the end of all readers
		if (null == bi) {
			if (null != lastException) {
				throw lastException;
			}
		}

		return bi;
	}

	/**
	 * Writes an image using the an arbitrary <code>ImageWriter</code> that supports the given format to an
	 * <code>ImageOutputStream</code>. The image is written to the <code>ImageOutputStream</code> 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</code> after the write
	 * operation has completed; it is the responsibility of the caller to close the stream, if desired.
	 * 
	 * @param im
	 *            a <code>RenderedImage</code> to be written.
	 * @param formatName
	 *            a <code>String</code> containg the informal name of the format.
	 * @param output
	 *            an <code>ImageOutputStream</code> to be written to.
	 * 
	 * @return <code>false</code> if no appropriate writer is found.
	 * 
	 * @exception IllegalArgumentException
	 *                if any parameter is <code>null</code>.
	 * @exception 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!");
		}

		ImageWriter writer = null;
		ImageTypeSpecifier type = ImageTypeSpecifier.createFromRenderedImage(im);
		Iterator iter = getImageWriters(type, formatName);
		if (iter.hasNext()) {
			writer = (ImageWriter) iter.next();
		}
		if (writer == null) {
			return false;
		}

		writer.setOutput(output);
		try {
			writer.write(im);
		} finally {
			writer.dispose();
			output.flush();
		}

		return true;
	}

	/**
	 * Writes an image using an arbitrary <code>ImageWriter</code> that supports the given format to a
	 * <code>File</code>. If there is already a <code>File</code> present, its contents are discarded.
	 * 
	 * @param im
	 *            a <code>RenderedImage</code> to be written.
	 * @param formatName
	 *            a <code>String</code> containg the informal name of the format.
	 * @param output
	 *            a <code>File</code> to be written to.
	 * 
	 * @return <code>false</code> if no appropriate writer is found.
	 * 
	 * @exception IllegalArgumentException
	 *                if any parameter is <code>null</code>.
	 * @exception IOException
	 *                if an error occurs during writing.
	 */
	public static boolean write(RenderedImage im, String formatName, File output) throws IOException {
		if (output == null) {
			throw new IllegalArgumentException("output == null!");
		}
		ImageOutputStream stream = null;
		try {
			output.delete();
			stream = createImageOutputStream(output);
		} catch (IOException e) {
			throw new IIOException("Can't create output stream!", e);
		}

		boolean val;
		try {
			val = write(im, formatName, stream);
		} finally {
			stream.close();
		}
		return val;
	}

	/**
	 * Writes an image using an arbitrary <code>ImageWriter</code> that supports the given format to an
	 * <code>OutputStream</code>.
	 * 
	 * <p>
	 * This method <em>does not</em> close the provided <code>OutputStream</code> 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</code>and <code>getCacheDirectory</code> will be used
	 * to control caching.
	 * 
	 * @param im
	 *            a <code>RenderedImage</code> to be written.
	 * @param formatName
	 *            a <code>String</code> containg the informal name of the format.
	 * @param output
	 *            an <code>OutputStream</code> to be written to.
	 * 
	 * @return <code>false</code> if no appropriate writer is found.
	 * 
	 * @exception IllegalArgumentException
	 *                if any parameter is <code>null</code>.
	 * @exception IOException
	 *                if an error occurs during writing.
	 */
	public static boolean write(RenderedImage im, String formatName, OutputStream output) throws IOException {
		if (output == null) {
			throw new IllegalArgumentException("output == null!");
		}
		ImageOutputStream stream = null;
		try {
			stream = createImageOutputStream(output);
		} catch (IOException e) {
			throw new IIOException("Can't create output stream!", e);
		}

		boolean val;
		try {
			val = write(im, formatName, stream);
		} finally {
			stream.close();
		}
		return val;
	}
}