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