/*******************************************************************************
 * Copyright (c) 2019 Red Hat and others. All rights reserved.
 * The contents of this file are made available under the terms
 * of the GNU Lesser General Public License (LGPL) Version 2.1 that
 * accompanies this distribution (lgpl-v21.txt).  The LGPL is also
 * available at http://www.gnu.org/licenses/lgpl.html.  If the version
 * of the LGPL at http://www.gnu.org is different to the version of
 * the LGPL accompanying this distribution and there is any conflict
 * between the two license versions, the terms of the LGPL accompanying
 * this distribution shall govern.
 *
 * Contributors:
 *     Red Hat - initial API and implementation
 *******************************************************************************/
package org.eclipse.swt.graphics;


import java.io.*;
import java.util.*;

import org.eclipse.swt.*;
import org.eclipse.swt.internal.*;
import org.eclipse.swt.internal.gtk.*;
import org.eclipse.swt.internal.image.*;

/**
 * Instances of this class are used to load images from,
 * and save images to, a file or stream.
 * <p>
 * Currently supported image formats are:
 * </p><ul>
 * <li>BMP (Windows or OS/2 Bitmap)</li>
 * <li>ICO (Windows Icon)</li>
 * <li>JPEG</li>
 * <li>GIF</li>
 * <li>PNG</li>
 * <li>TIFF</li>
 * </ul>
 * <code>ImageLoaders</code> can be used to:
 * <ul>
 * <li>load/save single images in all formats</li>
 * <li>load/save multiple images (GIF/ICO/TIFF)</li>
 * <li>load/save animated GIF images</li>
 * <li>load interlaced GIF/PNG images</li>
 * <li>load progressive JPEG images</li>
 * </ul>
 *
 * <p>
 * NOTE: <code>ImageLoader</code> is implemented in Java on some platforms, which has
 * certain performance implications. Performance and memory sensitive applications may
 * benefit from using one of the constructors provided by <code>Image</code>, as these
 * are implemented natively.</p>
 *
 * @see <a href="http://www.eclipse.org/swt/examples.php">SWT Example: ImageAnalyzer</a>
 * @see <a href="http://www.eclipse.org/swt/">Sample code and further information</a>
 */
public class ImageLoader {

	/**
	 * the array of ImageData objects in this ImageLoader.
	 * This array is read in when the load method is called,
	 * and it is written out when the save method is called
	 */
	public ImageData[] data;

	/**
	 * the width of the logical screen on which the images
	 * reside, in pixels (this corresponds to the GIF89a
	 * Logical Screen Width value)
	 */
	public int logicalScreenWidth;

	/**
	 * the height of the logical screen on which the images
	 * reside, in pixels (this corresponds to the GIF89a
	 * Logical Screen Height value)
	 */
	public int logicalScreenHeight;

	/**
	 * the background pixel for the logical screen (this
	 * corresponds to the GIF89a Background Color Index value).
	 * The default is -1 which means 'unspecified background'
	 *
	 */
	public int backgroundPixel;

	/**
	 * the number of times to repeat the display of a sequence
	 * of animated images (this corresponds to the commonly-used
	 * GIF application extension for "NETSCAPE 2.0 01").
	 * The default is 1. A value of 0 means 'display repeatedly'
	 */
	public int repeatCount;

	/**
	 * This is the compression used when saving jpeg and png files.
	 * <p>
	 * When saving jpeg files, the value is from 1 to 100,
	 * where 1 is very high compression but low quality, and 100 is
	 * no compression and high quality; default is 75.
	 * </p><p>
	 * When saving png files, the value is from 0 to 3, but they do not impact the quality
	 * because PNG is lossless compression. 0 is uncompressed, 1 is low compression and fast,
	 * 2 is default compression, and 3 is high compression but slow.
	 * </p>
	 *
	 * @since 3.8
	 */
	public int compression;

	/**
	 * If the 29th byte of the PNG file is not zero, then it is interlaced.
	 */
	final static int PNG_INTERLACE_METHOD_OFFSET = 28;

	/*
	 * the set of ImageLoader event listeners, created on demand
	 */
	List<ImageLoaderListener> imageLoaderListeners;

/**
 * Construct a new empty ImageLoader.
 */
public ImageLoader() {
	reset();
}

/**
 * Resets the fields of the ImageLoader, except for the
 * <code>imageLoaderListeners</code> field.
 */
void reset() {
	data = null;
	logicalScreenWidth = 0;
	logicalScreenHeight = 0;
	backgroundPixel = -1;
	repeatCount = 1;
	compression = -1;
}

/**
 * Loads an array of <code>ImageData</code> objects from the
 * specified input stream. Throws an error if either an error
 * occurs while loading the images, or if the images are not
 * of a supported type. Returns the loaded image data array.
 *
 * @param stream the input stream to load the images from
 * @return an array of <code>ImageData</code> objects loaded from the specified input stream
 *
 * @exception IllegalArgumentException <ul>
 *    <li>ERROR_NULL_ARGUMENT - if the stream is null</li>
 * </ul>
 * @exception SWTException <ul>
 *    <li>ERROR_IO - if an IO error occurs while reading from the stream</li>
 *    <li>ERROR_INVALID_IMAGE - if the image stream contains invalid data</li>
 *    <li>ERROR_UNSUPPORTED_FORMAT - if the image stream contains an unrecognized format</li>
 * </ul>
 */
public ImageData[] load(InputStream stream) {
	if (stream == null) SWT.error(SWT.ERROR_NULL_ARGUMENT);
	reset();
	ImageData [] imgDataArray = getImageDataArrayFromStream(stream);
	data = imgDataArray;
	return imgDataArray;
}

/**
 * Return true if the image is an interlaced PNG file.
 * This is used to check whether ImageLoaderEvent should be fired when loading images.
 * @param imageAsByteArray
 * @return true iff 29th byte of PNG files is not zero
 */
boolean isInterlacedPNG(byte [] imageAsByteArray) {
	return imageAsByteArray.length > PNG_INTERLACE_METHOD_OFFSET && imageAsByteArray[PNG_INTERLACE_METHOD_OFFSET] != 0;
}

ImageData [] getImageDataArrayFromStream(InputStream stream) {
	byte[] buffer = new byte[2048];
	long loader = GDK.gdk_pixbuf_loader_new();
	int length;
	List<ImageData> imgDataList = new ArrayList<>();
	try {
		// 1) Load InputStream into byte array
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		while ((length = stream.read(buffer)) > -1) {
			baos.write(buffer, 0, length);
		}
		baos.flush();
		byte[] data_buffer = baos.toByteArray();
		if (data_buffer.length == 0) SWT.error(SWT.ERROR_UNSUPPORTED_FORMAT);	// empty stream

		// 2) Copy byte array to C memory, write to GdkPixbufLoader
		long buffer_ptr = OS.g_malloc(data_buffer.length);
		C.memmove(buffer_ptr, data_buffer, data_buffer.length);
		GDK.gdk_pixbuf_loader_write(loader, buffer_ptr, data_buffer.length, null);
		GDK.gdk_pixbuf_loader_close(loader, null);

		// 3) Get GdkPixbufAnimation from loader
		long pixbuf_animation = GDK.gdk_pixbuf_loader_get_animation(loader);
		if (pixbuf_animation == 0) SWT.error(SWT.ERROR_INVALID_IMAGE);

		boolean isStatic = GDK.gdk_pixbuf_animation_is_static_image(pixbuf_animation);
		if (isStatic) {
			// Static image, get as single pixbuf and convert it to ImageData
			long pixbuf = GDK.gdk_pixbuf_animation_get_static_image(pixbuf_animation);
			ImageData imgData = pixbufToImageData(pixbuf);
			imgData.type = getImageFormat(loader);
			imgDataList.add(imgData);
		} else {
			// Image with multiple frames, iterate through each frame and convert
			// each frame to ImageData
			long start_time = OS.g_malloc(8);
			OS.g_get_current_time(start_time);
			long animation_iter = GDK.gdk_pixbuf_animation_get_iter (pixbuf_animation, start_time);
			int delay_time = 0;
			int time_offset = 0;
			// Fix the number of GIF frames as GdkPixbufAnimation does not provide an API to
			// determine number of frames.
			int num_frames = 32;
			for (int i = 0; i < num_frames; i++) {
				// Calculate time offset from start_time to next frame
				delay_time = GDK.gdk_pixbuf_animation_iter_get_delay_time (animation_iter);
				time_offset += delay_time;
				OS.g_time_val_add(start_time, time_offset * 1000);
				boolean update = GDK.gdk_pixbuf_animation_iter_advance (animation_iter, start_time);
				if (update) {
					long curr_pixbuf = GDK.gdk_pixbuf_animation_iter_get_pixbuf (animation_iter);
					long pixbuf_copy = GDK.gdk_pixbuf_copy(curr_pixbuf); // copy because curr_pixbuf might get disposed on next advance
					ImageData imgData = pixbufToImageData(pixbuf_copy);
					if (this.logicalScreenHeight == 0 && this.logicalScreenWidth == 0) {
						this.logicalScreenHeight = imgData.height;
						this.logicalScreenWidth = imgData.width;
					}
					OS.g_object_unref(pixbuf_copy);
					imgData.type = getImageFormat(loader);
					imgData.delayTime = delay_time;
					imgDataList.add(imgData);
				} else {
					break;
				}
			}
		}
		ImageData [] imgDataArray = new ImageData [imgDataList.size()];
		for (int i = 0; i < imgDataList.size(); i++) {
			imgDataArray [i] = imgDataList.get(i);
			// Loading completed, notify listeners
			// listener should only be called when loading interlaced/progressive PNG/JPG/GIF ?
			ImageData data = (ImageData) imgDataArray [i].clone();
			if (this.hasListeners() && imgDataArray != null) {
				if (data.type == SWT.IMAGE_PNG && isInterlacedPNG(data_buffer)) {
					this.notifyListeners(new ImageLoaderEvent(this, data, i, true));
				} else if (data.type != SWT.IMAGE_PNG) {
					this.notifyListeners(new ImageLoaderEvent(this, data, i, true));
				}
			}
		}
		OS.g_free(buffer_ptr);
		OS.g_object_unref(loader);
		stream.close();
		return imgDataArray;
	} catch (IOException e) {
		SWT.error(SWT.ERROR_IO);
	}
	return null;
}

/**
 * Loads an array of <code>ImageData</code> objects from the
 * file with the specified name. Throws an error if either
 * an error occurs while loading the images, or if the images are
 * not of a supported type. Returns the loaded image data array.
 *
 * @param filename the name of the file to load the images from
 * @return an array of <code>ImageData</code> objects loaded from the specified file
 *
 * @exception IllegalArgumentException <ul>
 *    <li>ERROR_NULL_ARGUMENT - if the file name is null</li>
 * </ul>
 * @exception SWTException <ul>
 *    <li>ERROR_IO - if an IO error occurs while reading from the file</li>
 *    <li>ERROR_INVALID_IMAGE - if the image file contains invalid data</li>
 *    <li>ERROR_UNSUPPORTED_FORMAT - if the image file contains an unrecognized format</li>
 * </ul>
 */
public ImageData[] load(String filename) {
	if (filename == null) SWT.error(SWT.ERROR_NULL_ARGUMENT);
	InputStream stream = null;
	try {
		stream = new FileInputStream(filename);
		return load(stream);
	} catch (IOException e) {
		SWT.error(SWT.ERROR_IO, e);
	} finally {
		try {
			if (stream != null) stream.close();
		} catch (IOException e) {
			// Ignore error
		}
	}
	return null;
}

/**
 * Load GdkPixbuf directly using gdk_pixbuf_new_from_file,
 * without FileInputStream.
 * @param filename
 * @return
 */
ImageData[] loadFromFile(String filename) {
	long pixbuf = gdk_pixbuf_new_from_file(filename);
	if (pixbuf == 0) return null;
	ImageData imgData= pixbufToImageData(pixbuf);
	return data = new ImageData[] {imgData};
}

/**
 * Return the type of file from which the image was read
 * by inspecting GdkPixbufFormat from GdkPixbufLoader
 *
 * It is expressed as one of the following values:
 * <dl>
 * <dt><code>IMAGE_BMP</code></dt>
 * <dd>Windows BMP file format, no compression</dd>
 * <dt><code>IMAGE_BMP_RLE</code></dt>
 * <dd>Windows BMP file format, RLE compression if appropriate</dd>
 * <dt><code>IMAGE_GIF</code></dt>
 * <dd>GIF file format</dd>
 * <dt><code>IMAGE_ICO</code></dt>
 * <dd>Windows ICO file format</dd>
 * <dt><code>IMAGE_JPEG</code></dt>
 * <dd>JPEG file format</dd>
 * <dt><code>IMAGE_PNG</code></dt>
 * <dd>PNG file format</dd>
 * </dl>
 */
int getImageFormat(long loader) {
	long format = GDK.gdk_pixbuf_loader_get_format(loader);
	long name = GDK.gdk_pixbuf_format_get_name(format);
	String nameStr = Converter.cCharPtrToJavaString(name, false);
	OS.g_free(name);
	switch (nameStr) {
		case "bmp": return SWT.IMAGE_BMP;
		case "gif": return SWT.IMAGE_GIF;
		case "ico": return SWT.IMAGE_ICO;
		case "jpeg": return SWT.IMAGE_JPEG;
		case "png": return SWT.IMAGE_PNG;
		case "svg": return SWT.IMAGE_SVG;
		default: return SWT.IMAGE_UNDEFINED;
	}
}

/**
 * Convert GdkPixbuf pointer to Java object ImageData
 * @param pixbuf
 * @return ImageData with pixbuf data
 */
static ImageData pixbufToImageData(long pixbuf) {
	boolean hasAlpha = GDK.gdk_pixbuf_get_has_alpha(pixbuf);
	int width = GDK.gdk_pixbuf_get_width(pixbuf);
	int height = GDK.gdk_pixbuf_get_height(pixbuf);
	int stride = GDK.gdk_pixbuf_get_rowstride(pixbuf);
	int n_channels = GDK.gdk_pixbuf_get_n_channels(pixbuf); 			// only 3 or 4 samples per pixel are supported
	int bits_per_sample = GDK.gdk_pixbuf_get_bits_per_sample(pixbuf); 	// only 8 bit per sample are supported
	long pixels = GDK.gdk_pixbuf_get_pixels(pixbuf);
	/*
	 * From GDK Docs: last row in the pixbuf may not be as wide as the full rowstride,
	 * but rather just as wide as the pixel data needs to be. Compute the width in bytes
	 * of the last row to copy raw pixbuf data.
	 */
	int lastRowWidth = width * ((n_channels * bits_per_sample + 7) / 8);
	byte[] srcData = new byte[stride * height];
	C.memmove(srcData, pixels, stride * (height - 1) + lastRowWidth);
	/*
	 * Note: GdkPixbuf only supports 3/4 n_channels and 8 bits_per_sample,
	 * This means all images are of depth 24 / depth 32. This means loading
	 * images will result in a direct PaletteData with RGB masks, since
	 * there is no way to determine indexed PaletteData info.
	 *
	 * See https://www.eclipse.org/articles/Article-SWT-images/graphics-resources.html#PaletteData
	 */
	PaletteData palette = new PaletteData(0xFF0000, 0xFF00, 0xFF);
	ImageData imgData = new ImageData(width, height, bits_per_sample * n_channels, palette, stride, srcData);
	if (hasAlpha) {
		byte[] alphaData = imgData.alphaData = new byte[width * height];
		for (int y = 0, offset = 0, alphaOffset = 0; y < height; y++) {
			for (int x = 0; x < width; x++, offset += n_channels) {
				byte r = srcData[offset + 0];
				byte g = srcData[offset + 1];
				byte b = srcData[offset + 2];
				byte a = srcData[offset + 3];
				srcData[offset + 0] = 0;
				alphaData[alphaOffset++] = a;
				if (a != 0) {
					srcData[offset + 1] = r;
					srcData[offset + 2] = g;
					srcData[offset + 3] = b;
				}
			}
		}
	} else {
		for (int y = 0, offset = 0; y < height; y++) {
			for (int x = 0; x < width; x++, offset += n_channels) {
				byte r = srcData[offset + 0];
				byte g = srcData[offset + 1];
				byte b = srcData[offset + 2];
				srcData[offset + 0] = r;
				srcData[offset + 1] = g;
				srcData[offset + 2] = b;
			}
		}
	}
	return imgData;
}

/**
 * Returns GdkPixbuf pointer by loading an image from filename (Java string)
 * @param filename
 * @return
 */
static long gdk_pixbuf_new_from_file(String filename) {
	int length = filename.length ();
	char [] chars = new char [length];
	filename.getChars (0, length, chars, 0);
	byte [] buffer = Converter.wcsToMbcs(chars, true);
	return GDK.gdk_pixbuf_new_from_file(buffer, null);
}

/**
 * Saves the image data in this ImageLoader to the specified stream.
 * The format parameter can have one of the following values:
 * <dl>
 * <dt><code>IMAGE_BMP</code></dt>
 * <dd>Windows BMP file format, no compression</dd>
 * <dt><code>IMAGE_BMP_RLE</code></dt>
 * <dd>Windows BMP file format, RLE compression if appropriate</dd>
 * <dt><code>IMAGE_GIF</code></dt>
 * <dd>GIF file format</dd>
 * <dt><code>IMAGE_ICO</code></dt>
 * <dd>Windows ICO file format</dd>
 * <dt><code>IMAGE_JPEG</code></dt>
 * <dd>JPEG file format</dd>
 * <dt><code>IMAGE_PNG</code></dt>
 * <dd>PNG file format</dd>
 * </dl>
 *
 * @param stream the output stream to write the images to
 * @param format the format to write the images in
 *
 * @exception IllegalArgumentException <ul>
 *    <li>ERROR_NULL_ARGUMENT - if the stream is null</li>
 * </ul>
 * @exception SWTException <ul>
 *    <li>ERROR_IO - if an IO error occurs while writing to the stream</li>
 *    <li>ERROR_INVALID_IMAGE - if the image data contains invalid data</li>
 *    <li>ERROR_UNSUPPORTED_FORMAT - if the image data cannot be saved to the requested format</li>
 * </ul>
 */
public void save(OutputStream stream, int format) {
	if (stream == null) SWT.error(SWT.ERROR_NULL_ARGUMENT);
	if (format == -1) SWT.error(SWT.ERROR_UNSUPPORTED_FORMAT);
	if (this.data == null) SWT.error(SWT.ERROR_NULL_ARGUMENT);
	ImageData imgData = this.data [0];
	int colorspace = GDK.GDK_COLORSPACE_RGB;
	boolean alpha_supported = format == SWT.IMAGE_TIFF || format == SWT.IMAGE_PNG || format == SWT.IMAGE_ICO;
	boolean has_alpha = imgData.alphaData != null && alpha_supported;
	int width = imgData.width;
	int height = imgData.height;
	// original n_channels. Native implementation will only be used in case of 3 or 4
	int n_channels = imgData.bytesPerLine / width;

	// Native implementation only supports a subset of possible image configurations.
	// Redirect the not supported variants to the Java implementation.
	// See also https://bugs.eclipse.org/bugs/show_bug.cgi?id=558043
	if (!imgData.palette.isDirect || n_channels < 3 || n_channels > 4) {
		FileFormat.save(stream, format, this);
		return;
	}

	/*
	 * Destination offsets, GdkPixbuf data is stored in RGBA format.
	 */
	int da = 3; int dr = 0; int dg = 1; int db = 2;

	/*
	 * ImageData offsets. These can vary depending on how the ImageData.data
	 * field was populated. In most cases it will be RGB format, so this case
	 * is assumed (blue shift is 0).
	 *
	 * If blue is negatively shifted, then we are dealing with BGR byte ordering, so
	 * adjust the offsets accordingly.
	 */
	int or = 0; int og = 1; int ob = 2;
	PaletteData palette = imgData.palette;
	if (palette.isDirect && palette.blueShift < 0) {
		or = 2;
		og = 1;
		ob = 0;
	}

	// We use alpha by default now so all images saved are 32 bit, if there is no alpha we set it to 255
	int bytes_per_pixel = 4;
	byte[] srcData = new byte[(width * height * bytes_per_pixel)];

	int alpha_offset = n_channels == 4 ? 1 : 0;
	if (has_alpha) {
		for (int y = 0, offset = 0, new_offset = 0, alphaIndex = 0; y < height; y++) {
			for (int x = 0; x < width; x++, offset += n_channels, new_offset += bytes_per_pixel) {
				byte a = imgData.alphaData[alphaIndex++];
				byte r = imgData.data[offset + alpha_offset + or];
				byte g = imgData.data[offset + alpha_offset + og];
				byte b = imgData.data[offset + alpha_offset + ob];

				// GdkPixbuf expects RGBA format
				srcData[new_offset + db] = b;
				srcData[new_offset + dg] = g;
				srcData[new_offset + dr] = r;
				srcData[new_offset + da] = a;
			}
		}
	} else {
		for (int y = 0, offset = 0, new_offset = 0; y < height; y++) {
			for (int x = 0; x < width; x++, offset += n_channels, new_offset += bytes_per_pixel) {
				byte r = imgData.data[offset + alpha_offset + or];
				byte g = imgData.data[offset + alpha_offset + og];
				byte b = imgData.data[offset + alpha_offset + ob];
				byte a = (byte) 255;

				srcData[new_offset + db] = b;
				srcData[new_offset + dg] = g;
				srcData[new_offset + dr] = r;
				srcData[new_offset + da] = a;
			}
		}
	}

	// Get GdkPixbuf from pixel data buffer
	long buffer_ptr = OS.g_malloc(srcData.length);
	C.memmove(buffer_ptr, srcData, srcData.length);
	int rowstride = srcData.length / height;
	// We use alpha in all cases, if no alpha is provided then it's just 255
	long pixbuf = GDK.gdk_pixbuf_new_from_data (buffer_ptr, colorspace, true, 8, width, height, rowstride, 0, 0);
	if (pixbuf == 0) {
		OS.g_free(buffer_ptr);
		SWT.error(SWT.ERROR_NULL_ARGUMENT);
	}

	// Write pixbuf to byte array and then to OutputStream
	String typeStr = "";
	switch (format) {
		case SWT.IMAGE_BMP_RLE: typeStr = "bmp"; break;
		case SWT.IMAGE_BMP: typeStr = "bmp"; break;
		case SWT.IMAGE_GIF: typeStr = "gif"; break;
		case SWT.IMAGE_ICO: typeStr = "ico"; break;
		case SWT.IMAGE_JPEG: typeStr = "jpeg"; break;
		case SWT.IMAGE_PNG: typeStr = "png"; break;
		case SWT.IMAGE_TIFF: typeStr = "tiff"; break;
		case SWT.IMAGE_SVG: typeStr = "svg"; break;
	}
	byte [] type = Converter.wcsToMbcs(typeStr, true);

	long [] buffer = new long [1];
	if (type == null || typeStr == "") {
		OS.g_free(buffer_ptr);
		SWT.error(SWT.ERROR_UNSUPPORTED_FORMAT);
	}
	long [] len = new long [1];
	GDK.gdk_pixbuf_save_to_bufferv(pixbuf, buffer, len, type, null, null, null);
	byte[] byteArray = new byte[(int) len[0]];
	C.memmove(byteArray, buffer[0], byteArray.length);
	try {
		stream.write(byteArray);
	} catch (IOException e) {
		OS.g_free(buffer_ptr);
		SWT.error(SWT.ERROR_IO);
	}
	// must free buffer_ptr last otherwise we get half/corrupted image
	OS.g_free(buffer_ptr);
}

/**
 * Saves the image data in this ImageLoader to a file with the specified name.
 * The format parameter can have one of the following values:
 * <dl>
 * <dt><code>IMAGE_BMP</code></dt>
 * <dd>Windows BMP file format, no compression</dd>
 * <dt><code>IMAGE_BMP_RLE</code></dt>
 * <dd>Windows BMP file format, RLE compression if appropriate</dd>
 * <dt><code>IMAGE_GIF</code></dt>
 * <dd>GIF file format</dd>
 * <dt><code>IMAGE_ICO</code></dt>
 * <dd>Windows ICO file format</dd>
 * <dt><code>IMAGE_JPEG</code></dt>
 * <dd>JPEG file format</dd>
 * <dt><code>IMAGE_PNG</code></dt>
 * <dd>PNG file format</dd>
 * </dl>
 *
 * @param filename the name of the file to write the images to
 * @param format the format to write the images in
 *
 * @exception IllegalArgumentException <ul>
 *    <li>ERROR_NULL_ARGUMENT - if the file name is null</li>
 * </ul>
 * @exception SWTException <ul>
 *    <li>ERROR_IO - if an IO error occurs while writing to the file</li>
 *    <li>ERROR_INVALID_IMAGE - if the image data contains invalid data</li>
 *    <li>ERROR_UNSUPPORTED_FORMAT - if the image data cannot be saved to the requested format</li>
 * </ul>
 */
public void save(String filename, int format) {
	if (filename == null) SWT.error(SWT.ERROR_NULL_ARGUMENT);
	OutputStream stream = null;
	try {
		stream = new FileOutputStream(filename);
	} catch (IOException e) {
		SWT.error(SWT.ERROR_IO, e);
	}
	save(stream, format);
	try {
		stream.close();
	} catch (IOException e) {
	}
}

/**
 * Adds the listener to the collection of listeners who will be
 * notified when image data is either partially or completely loaded.
 * <p>
 * An ImageLoaderListener should be added before invoking
 * one of the receiver's load methods. The listener's
 * <code>imageDataLoaded</code> method is called when image
 * data has been partially loaded, as is supported by interlaced
 * GIF/PNG or progressive JPEG images.
 *
 * @param listener the listener which should be notified
 *
 * @exception IllegalArgumentException <ul>
 *    <li>ERROR_NULL_ARGUMENT - if the listener is null</li>
 * </ul>
 *
 * @see ImageLoaderListener
 * @see ImageLoaderEvent
 */
public void addImageLoaderListener(ImageLoaderListener listener) {
	if (listener == null) SWT.error (SWT.ERROR_NULL_ARGUMENT);
	if (imageLoaderListeners == null) {
		imageLoaderListeners = new ArrayList<>();
	}
	imageLoaderListeners.add(listener);
}

/**
 * Removes the listener from the collection of listeners who will be
 * notified when image data is either partially or completely loaded.
 *
 * @param listener the listener which should no longer be notified
 *
 * @exception IllegalArgumentException <ul>
 *    <li>ERROR_NULL_ARGUMENT - if the listener is null</li>
 * </ul>
 *
 * @see #addImageLoaderListener(ImageLoaderListener)
 */
public void removeImageLoaderListener(ImageLoaderListener listener) {
	if (listener == null) SWT.error (SWT.ERROR_NULL_ARGUMENT);
	if (imageLoaderListeners == null) return;
	imageLoaderListeners.remove(listener);
}

/**
 * Returns <code>true</code> if the receiver has image loader
 * listeners, and <code>false</code> otherwise.
 *
 * @return <code>true</code> if there are <code>ImageLoaderListener</code>s, and <code>false</code> otherwise
 *
 * @see #addImageLoaderListener(ImageLoaderListener)
 * @see #removeImageLoaderListener(ImageLoaderListener)
 */
public boolean hasListeners() {
	return imageLoaderListeners != null && imageLoaderListeners.size() > 0;
}

/**
 * Notifies all image loader listeners that an image loader event
 * has occurred. Pass the specified event object to each listener.
 *
 * @param event the <code>ImageLoaderEvent</code> to send to each <code>ImageLoaderListener</code>
 */
public void notifyListeners(ImageLoaderEvent event) {
	if (!hasListeners()) return;
	int size = imageLoaderListeners.size();
	for (int i = 0; i < size; i++) {
		ImageLoaderListener listener = imageLoaderListeners.get(i);
		listener.imageDataLoaded(event);
	}
}

}
