org.eclipse.swt.graphics.ImageLoader.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.swt.graphics.ImageLoader.java

Source

/*******************************************************************************
 * 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.*;

/**
 * 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);
        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;
        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>
     * <dt><code>IMAGE_TIFF</code></dt>
     * <dd>TIFF 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;
        int n_channels = imgData.bytesPerLine / width; // original n_channels 3 or 4
        int bytes_per_pixel = imgData.bytesPerLine / width; // n_channels for original ImageData (width * height * bytes_per_pixel) = imgData.length

        /*
         * 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;
        }

        if (has_alpha && bytes_per_pixel == 3) {
            bytes_per_pixel = 4;
        }

        // We use alpha by default now so just hard code bytes per pixel to 4
        byte[] srcData = new byte[(width * height * 4)];

        int alpha_offset = n_channels == 4 ? 1 : 0;
        if (has_alpha) {
            for (int y = 0, offset = 0, new_offset = 0, alphaOffset = 0; y < height; y++) {
                for (int x = 0; x < width; x++, offset += n_channels, new_offset += bytes_per_pixel) {
                    byte a = imgData.alphaData[alphaOffset++];
                    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;
        }
        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>
     * <dt><code>IMAGE_TIFF</code></dt>
     * <dd>TIFF 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);
        }
    }

}