All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.numdata.oss.ui.ImageTools Maven / Gradle / Ivy

There is a newer version: 1.22
Show newest version
/*
 * Copyright (c) 2017, Numdata BV, The Netherlands.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of Numdata nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL NUMDATA BV BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.numdata.oss.ui;

import java.awt.*;
import java.awt.image.*;
import java.io.*;
import java.net.*;
import java.util.*;
import java.util.List;
import javax.imageio.*;
import javax.imageio.stream.*;
import javax.swing.*;

import com.numdata.oss.io.*;
import com.numdata.oss.log.*;
import org.jetbrains.annotations.*;

/**
 * This class provides an (AWT) image toolbox. It also provides a central point
 * for accessing shared image resources.
 *
 * This code does not depend on any API outside this project or the Java2
 * standard.
 *
 * @author Peter S. Heijnen
 */
@SuppressWarnings( { "PublicField", "ConstantNamingConvention", "StaticNonFinalField" } )
public class ImageTools
{
	/**
	 * Debug flag to delay image loading operations to simulate low bandwidth
	 * network connections.
	 */
	public static final long LOAD_DELAY = 0L;

	/**
	 * Mode for scaling an image.
	 */
	public enum ScaleMode
	{
		/**
		 * Use the original image size.
		 */
		NONE,

		/**
		 * Scale the image to cover the entire component, while preserving the
		 * image's aspect ratio.
		 */
		COVER,

		/**
		 * Scale the image to fit inside the component, while preserving the
		 * image's aspect ratio.
		 */
		CONTAIN,

		/**
		 * Scale the image to fit the entire component, without preserving the
		 * image's aspect ratio.
		 */
		STRETCH
	}

	/**
	 * Log used for messages related to this class.
	 */
	private static final ClassLogger LOG = ClassLogger.getFor( ImageTools.class );

	/**
	 * Switch to enable warning messages (on {@link System#err}) when images are
	 * not found. This may be very useful for tracing program bugs or deployment
	 * errors.
	 *
	 * This option can be set using the {@code ImageTools.missingImageWarnings}
	 * system property.
	 */
	public static boolean missingImageWarningsEnabled = true;

	/**
	 * Show warning is image is loaded on EDT. Since image loading can be time
	 * consuming, loading them on the EDT may disrupt responsiveness of the
	 * application. Loading should be deferred to worker threads instead.
	 *
	 * This option can be set using the {@code ImageTools.edtWarnings} system
	 * property.
	 */
	public static boolean edtWarningsEnabled = true;

	/**
	 * Show warning if icon is loaded without preset dimensions; if using preset
	 * dimensions, icons may be loaded asynchronously using the {@link
	 * AsyncIcon} class.
	 *
	 * This option can be set using the {@code ImageTools.presetIconSizeWarnings}
	 * system property.
	 */
	public static boolean presetIconSizeWarningsEnabled = true;

	/**
	 * Search path to use for loading image resources.
	 */
	private static final List _searchPath = new LinkedList();

	/**
	 * Class loader to load image resources.
	 */
	private static ClassLoader _classLoader = ImageTools.class.getClassLoader();

	/**
	 * Cache for previously loaded images.
	 *
	 * @see #getImage
	 */
	private static final Map _imageCache = new HashMap();

	/**
	 * Cache for resources relative to classes.
	 *
	 * @see #getClassResource(Class, String)
	 * @see #getImageIcon(Class, String)
	 */
	private static final Map, Map> _classResourceCache = new HashMap, Map>();

	static
	{
		missingImageWarningsEnabled = getSystemSetting( "missingImageWarnings", true );
		edtWarningsEnabled = getSystemSetting( "edtWarnings", true );
		presetIconSizeWarningsEnabled = getSystemSetting( "presetIconSizeWarnings", true );

	}

	/**
	 * Helper-method to get boolean system property. This supports 'yes'/'no';
	 * 'y'/'n'; '0'/'1'; 'on'/'off', and 'true'/'false' values. If none of these
	 * values is set or the system property is not set at all, the default value
	 * is returned.
	 *
	 * NOTE: This method is similar to {@link Boolean#getBoolean(String)}, but
	 * allows more value formats and supports a default value.
	 *
	 * @param name          Name of system property.
	 * @param defaultResult Default boolean value if no valid value is set.
	 *
	 * @return Boolean value.
	 */
	private static boolean getSystemSetting( final String name, final boolean defaultResult )
	{
		boolean result = defaultResult;

		try
		{
			final String value = System.getProperty( "ImageTools." + name );

			if ( "true".equalsIgnoreCase( value ) ||
			     "on".equalsIgnoreCase( value ) ||
			     "yes".equalsIgnoreCase( value ) ||
			     "y".equalsIgnoreCase( value ) ||
			     "1".equals( value ) )
			{
				result = true;
			}
			else if ( "false".equalsIgnoreCase( value ) ||
			          "off".equalsIgnoreCase( value ) ||
			          "no".equalsIgnoreCase( value ) ||
			          "n".equalsIgnoreCase( value ) ||
			          "0".equals( value ) )
			{
				result = false;
			}
		}
		catch ( final Throwable ignored )
		{
			/* access to system property denied */
		}

		return result;
	}

	/**
	 * Utility/Application class is not supposed to be instantiated.
	 */
	private ImageTools()
	{
	}

	/**
	 * Disable warning messages about requested but missing images (on* {@link
	 * System#err}. These messages can be very useful when tracing program bugs
	 * or deployment errors, but can be annoying in test environments.
	 */
	public static void disableMissingImageWarnings()
	{
		missingImageWarningsEnabled = false;
	}

	/**
	 * Clear search path.
	 */
	public static void clearSearchPath()
	{
		synchronized ( _searchPath )
		{
			_searchPath.clear();
		}
	}

	/**
	 * Add entry to image search path. If the entry already exists, it will be
	 * ignored.
	 *
	 * @param path Path entry to add to image search path.
	 */
	public static void addToSearchPath( final String path )
	{
		final String actualEntry;

		if ( path == null )
		{
			throw new NullPointerException( "path" );
		}

		final int len = path.length();
		if ( len == 0 )
		{
			throw new IllegalArgumentException( "0-length string is not allowed in search path" );
		}

		/*
		 * Add proper separator to path entry if needed. This can't be determined
		 * 100% sure, but should work well with Windows, Unix(-like), and URL's.
		 */
		int sepPos;
		if ( ( sepPos = path.lastIndexOf( (int)File.separatorChar ) ) >= 0 )
		{
			actualEntry = ( sepPos < len - 1 ) ? path + File.separatorChar : path;
		}
		else if ( ( sepPos = path.lastIndexOf( (int)'/' ) ) >= 0 )
		{
			actualEntry = ( sepPos != len - 1 ) ? path + '/' : path;
		}
		else
		{
			actualEntry = path + File.separatorChar;
		}

		final List searchPath = _searchPath;
		synchronized ( searchPath )
		{
			if ( !searchPath.contains( actualEntry ) )
			{
				searchPath.add( actualEntry );
			}
		}
	}

	/**
	 * Set class loader to load image resources.
	 *
	 * @param classLoader Class loader to load image resources.
	 */
	public static void setClassLoader( final ClassLoader classLoader )
	{
		_classLoader = classLoader;
	}


	/**
	 * Get {@link ImageIcon} for the given path. Resources are loaded against
	 * the specified {@code referenceClass}.
	 *
	 * @param referenceClass Class to resolve path against to and whose class
	 *                       loader to use.
	 * @param path           Image path.
	 *
	 * @return Icon object; {@code null} if image is not found.
	 */
	@Nullable
	public static ImageIcon getImageIcon( @NotNull final Class referenceClass, @Nullable final String path )
	{
		final ImageIcon result;

		final BufferedImage image = getImage( referenceClass, path );
		if ( image != null )
		{
			result = new ImageIcon( image );
			if ( presetIconSizeWarningsEnabled )
			{
				new Throwable( "[ImageTools: TODO: Load '" + path + "' icon with preset dimensions: " + result.getIconWidth() + " x " + result.getIconHeight() + ']' ).printStackTrace();
			}
		}
		else
		{
			result = null;
		}

		return result;
	}

	/**
	 * Get {@link ImageIcon} for the given path. Resources are loaded against
	 * the specified {@code referenceClass}.
	 *
	 * @param referenceClass Class to resolve path against to and whose class
	 *                       loader to use.
	 * @param path           Image path.
	 *
	 * @return Icon object; {@code null} if image is not found.
	 */
	@Nullable
	public static BufferedImage getImage( @NotNull final Class referenceClass, @Nullable final String path )
	{
		BufferedImage result = null;

		if ( path != null )
		{
			final URL url = getClassResource( referenceClass, path );
			if ( url != null )
			{
				result = getImage( url );
			}
			else
			{
				result = getImage( path );
			}
		}

		return result;
	}

	/**
	 * Get URL to resource relative to the given class.
	 *
	 * This method uses a permanent cache to speed up future requests.
	 *
	 * @param referenceClass Class to resolve path against to and whose class
	 *                       loader to use.
	 * @param path           Resource path.
	 *
	 * @return {@link URL} to resource; {@code null} if the resource was not
	 * found.
	 */
	@Nullable
	static URL getClassResource( @NotNull final Class referenceClass, @NotNull final String path )
	{
		URL result;

		final Map, Map> cache = _classResourceCache;
		synchronized ( cache )
		{
			final Map, Map> classResourceCache = cache;

			Map resources = classResourceCache.get( referenceClass );
			if ( resources == null )
			{
				resources = new HashMap();
				classResourceCache.put( referenceClass, resources );
				result = referenceClass.getResource( path );
				resources.put( path, result );
			}
			else
			{
				result = resources.get( path );
				if ( ( result == null ) && !resources.containsKey( path ) )
				{
					result = referenceClass.getResource( path );
					resources.put( path, result );
				}
			}
		}

		return result;
	}

	/**
	 * Get image for the specified URL.
	 *
	 * This method stores all returned images in a permanent cache! Please do
	 * not use this method for loading images that are rarely used.
	 *
	 * @param url URL to image file.
	 *
	 * @return Image object.
	 */
	@Nullable
	public static BufferedImage getImage( @Nullable final URL url )
	{
		BufferedImage result;

		if ( url == null )
		{
			result = null;
		}
		else
		{
			final String key = url.toExternalForm();
			final Map cache = _imageCache;

			final boolean needToLoad;

			synchronized ( cache )
			{
				result = cache.get( key );
				needToLoad = ( result == null ) && !cache.containsKey( key );
			}

			if ( needToLoad )
			{
				try
				{
					result = load( url );
				}
				catch ( final IOException e )
				{
					if ( missingImageWarningsEnabled )
					{
						new Throwable( "[Image not found: " + url + "] (" + e + ')' ).printStackTrace();
					}
					result = null;
				}

				synchronized ( cache )
				{
					cache.put( key, result );
				}
			}
		}

		return result;
	}

	/**
	 * Get image for the specified path.
	 *
	 * This method stores all returned images in a permanent cache! Please do
	 * not use this method for loading images that are rarely used (use the
	 * {@link #load(String)} method for that purpose).
	 *
	 * @param path Path to image file.
	 *
	 * @return Image object.
	 */
	@Nullable
	public static BufferedImage getImage( @Nullable final String path )
	{
		BufferedImage result;

		if ( ( path == null ) || path.isEmpty() )
		{
			result = null;
		}
		else
		{
			final Map cache = _imageCache;

			final boolean needToLoad;
			synchronized ( cache )
			{
				result = cache.get( path );
				needToLoad = ( result == null ) && !cache.containsKey( path );
			}

			if ( needToLoad )
			{
				result = load( path );

				synchronized ( cache )
				{
					cache.put( path, result );
				}
			}
		}
		return result;
	}

	/**
	 * Load the image with the specified path. This method will not cache
	 * images, use the {@link #getImage} method for that purpose.
	 *
	 * @param path Path to image file.
	 *
	 * @return Image object.
	 */
	public static BufferedImage load( final String path )
	{
		LOG.debug( "load( " + path + " )" );
		final boolean trace = LOG.isTraceEnabled();

		BufferedImage result = null;

		if ( ( path != null ) && !path.isEmpty() )
		{
			final boolean mightBeRelative = ( path.indexOf( ':' ) < 0 );
			final String relativePath = ( mightBeRelative && ( path.charAt( 0 ) == '/' ) || ( path.charAt( 0 ) == '\\' ) ) ? path.substring( 1 ) : path;

			{
				InputStream stream = null;
				try
				{
					if ( trace )
					{
						LOG.trace( " - try loading resource " + relativePath );
					}
					stream = _classLoader.getResourceAsStream( relativePath );
					if ( stream != null )
					{
						result = load( stream );
					}
				}
				catch ( final IOException e )
				{
					LOG.trace( "failed to retrieve: " + relativePath, e );
				}
				catch ( final SecurityException e )
				{
					LOG.trace( "failed to retrieve: " + relativePath, e );
				}
				finally
				{
					if ( stream != null )
					{
						if ( trace )
						{
							LOG.trace( "      > found resource " + relativePath );
						}
						try
						{
							stream.close();
						}
						catch ( final IOException e )
						{
							LOG.trace( "failed to close: " + relativePath, e );
						}
					}
				}
			}

			if ( result == null )
			{
				final List searchPath = _searchPath;
				synchronized ( searchPath )
				{
					final int last = mightBeRelative ? searchPath.size() : 0;

					for ( int i = 0; ( result == null ) && ( i <= last ); i++ )
					{
						final String actualPath = ( i < last ) ? searchPath.get( i ) + relativePath : path;
						if ( trace )
						{
							LOG.trace( " - try loading from " + actualPath );
						}

						try
						{
							if ( actualPath.contains( ":/" ) )
							{
								result = load( new URL( actualPath ) );
							}
							else
							{
								result = load( new File( actualPath ) );
							}
							if ( trace )
							{
								LOG.trace( "      > found  " + actualPath );
							}
						}
						catch ( final FileNotFoundException e )
						{
							LOG.trace( "failed to load: " + actualPath, e );
						}
						catch ( final IOException e )
						{
							LOG.trace( "failed to load: " + actualPath, e );
						}
						catch ( final SecurityException e )
						{
							LOG.trace( "failed to load: " + actualPath, e );
						}
					}
				}
			}

		}

		/*
		 * Provide clue to problems with loading the image.
		 */
		if ( result == null )
		{
			if ( missingImageWarningsEnabled )
			{
				new Throwable( "[Image not found: " + path + ']' ).printStackTrace();
			}
		}

		if ( ( result != null ) && edtWarningsEnabled && SwingUtilities.isEventDispatchThread() )
		{
			new Throwable( "[ImageTools: TODO: Should load '" + path + "' from worker thread, not the EDT!" + ( ( result != null ) ? " (loaded images size = " + result.getWidth() + " x " + result.getHeight() + ')' : "" ) + ']' ).printStackTrace();
		}

		if ( LOAD_DELAY > 0L )
		{
			System.err.println( "[ImageTools: load( '" + path + "') " + ( ( result != null ) ? "successful" : "failed" ) + ']' );
			try
			{
				Thread.sleep( ( result == null ) ? LOAD_DELAY / 3 : LOAD_DELAY );
			}
			catch ( final InterruptedException e )
			{
				e.printStackTrace();
			}
		}

		return result;
	}

	/**
	 * Read image from file.
	 *
	 * @param file File.
	 *
	 * @return {@link BufferedImage} that was loaded.
	 *
	 * @throws IOException if the image could not be loaded.
	 */
	@NotNull
	public static BufferedImage load( @NotNull final File file )
	throws IOException
	{
		return load( DataStreamTools.readByteArray( file ) );
	}

	/**
	 * Read image from input stream.
	 *
	 * @param in Input stream.
	 *
	 * @return {@link BufferedImage} that was loaded.
	 *
	 * @throws IOException if the image could not be loaded.
	 */
	@NotNull
	public static BufferedImage load( @NotNull final InputStream in )
	throws IOException
	{
		final byte[] imageData;
		try
		{
			imageData = DataStreamTools.readByteArray( in );
		}
		finally
		{
			in.close();
		}

		return load( imageData );
	}

	/**
	 * Read image from URL.
	 *
	 * @param url URL for image.
	 *
	 * @return {@link BufferedImage} that was loaded.
	 *
	 * @throws IOException if the image could not be loaded.
	 */
	@NotNull
	public static BufferedImage load( @NotNull final URL url )
	throws IOException
	{
		final byte[] imageData;

		final InputStream in = url.openStream();
		try
		{
			imageData = DataStreamTools.readByteArray( in );

		}
		finally
		{
			in.close();
		}

		return load( imageData );
	}

	/**
	 * Read image from image stream.
	 *
	 * @param imageInputStream Image stream.
	 *
	 * @return {@link BufferedImage} that was loaded.
	 *
	 * @throws IOException if the image could not be loaded.
	 */
	@NotNull
	public static BufferedImage load( @NotNull final ImageInputStream imageInputStream )
	throws IOException
	{
		byte[] imageData;
		try
		{
			imageData = null;
			final int length = (int)imageInputStream.length();
			byte[] buffer = new byte[ ( length >= 0 ) ? length : 1024 ]; // initial buffer size

			while ( true )
			{
				final int numberRead = imageInputStream.read( buffer );
				if ( numberRead == length )
				{
					imageData = buffer;
					break;
				}
				else if ( numberRead <= 0 )
				{
					if ( imageData == null )
					{
						throw new IOException( "Zero length image data" );
					}
					break;
				}
				else if ( imageData == null ) // create first result
				{
					imageData = new byte[ numberRead ];
					System.arraycopy( buffer, 0, imageData, 0, numberRead );
				}
				else // Create next buffer
				{
					final byte[] newTotal = new byte[ imageData.length + numberRead ];

					System.arraycopy( imageData, 0, newTotal, 0, imageData.length );
					System.arraycopy( buffer, 0, newTotal, imageData.length, numberRead );

					buffer = imageData;
					imageData = newTotal;
				}
			}
		}
		finally
		{
			imageInputStream.close();
		}

		return load( imageData );
	}

	/**
	 * Read image from image data.
	 *
	 * @param imageData Image data.
	 *
	 * @return {@link BufferedImage} that was loaded.
	 *
	 * @throws IOException if the image could not be loaded.
	 */
	@NotNull
	public static BufferedImage load( @NotNull final byte[] imageData )
	throws IOException
	{

		// Try to work around bugs in the JDK:
		//  - Bug 6986863: ProfileDeferralMgr throwing ConcurrentModificationException (https://bugs.openjdk.java.net/browse/JDK-6986863)
		//  - Bug 8032243: LCMS error 13: Couldn't link the profiles  (https://bugs.openjdk.java.net/browse/JDK-8032243)
		//
		// We try to work around these bugs by synchronizing on ImageIO (dirty!), basically making the JPEG decoding single-threaded.
		synchronized ( ImageIO.class )
		{
			final BufferedImage result = ImageIO.read( new ByteArrayInputStream( imageData ) );
			if ( result == null )
			{
				throw new IOException( "ImageIO failed to load image (" + imageData.length + " bytes)" );
			}

			return result;
		}
	}

	/**
	 * Returns a scaled instance of the given image. When the image size is
	 * reduced, a multi-step bilinear scaling method is used. This method avoids
	 * glitches that appear when using traditional bilinear or bicubic when an
	 * image is scaled to less than 50% its original size. When the image size
	 * is increased, bicubic interpolation is used instead.
	 *
	 * Based on code from an article published at 'java.net': 
	 * The Perils of Image.getScaledInstance().
	 *
	 * @param source       Image to be scaled.
	 * @param targetWidth  Width of the result, in pixels.
	 * @param targetHeight Height of the result, in pixels.
	 * @param scaleMode    Scale mode to use.
	 * @param background   Fill paint to use for background if keeping the
	 *                     aspect ratio requires 'empty' area; {@code null} will
	 *                     result in a smaller result image (empty areas are
	 *                     removed).
	 *
	 * @return Scaled version of the given image. If the source image already
	 * had the specified target size, the source image is returned.
	 */
	public static BufferedImage createScaledInstance( @NotNull final BufferedImage source, final int targetWidth, final int targetHeight, @NotNull final ScaleMode scaleMode, @Nullable final Paint background )
	{
		if ( ( source.getWidth() <= 0 ) || ( source.getHeight() <= 0 ) )
		{
			throw new IllegalArgumentException( "source image has invalid size: " + source.getWidth() + " x " + source.getHeight() );
		}

		if ( ( targetWidth <= 0 ) || ( targetHeight <= 0 ) )
		{
			throw new IllegalArgumentException( "invalid target size specified: " + targetWidth + " x " + targetHeight );
		}

		final boolean haveBackground = ( background != null );

		/*
		 * Determine appropriate image type.
		 */
		final ColorModel sourceColorModel = source.getColorModel();
		final int scaledType = sourceColorModel.hasAlpha() ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB;

		int scaledImageWidth = source.getWidth();
		int scaledImageHeight = source.getHeight();

		/*
		 * Determine width and height of the target image, taking the
		 * 'keepAspect' parameter into account.
		 */
		final int imageTargetWidth;
		final int imageTargetHeight;

		if ( scaleMode == ScaleMode.COVER )
		{
			final double targetScale = Math.max( (double)targetWidth / (double)scaledImageWidth,
			                                     (double)targetHeight / (double)scaledImageHeight );

			imageTargetWidth = (int)Math.round( (double)scaledImageWidth * targetScale );
			imageTargetHeight = (int)Math.round( (double)scaledImageHeight * targetScale );
		}
		else if ( scaleMode == ScaleMode.CONTAIN )
		{
			final double targetScale = Math.min( (double)targetWidth / (double)scaledImageWidth,
			                                     (double)targetHeight / (double)scaledImageHeight );

			imageTargetWidth = (int)Math.round( (double)scaledImageWidth * targetScale );
			imageTargetHeight = (int)Math.round( (double)scaledImageHeight * targetScale );
		}
		else if ( scaleMode == ScaleMode.STRETCH )
		{
			imageTargetWidth = targetWidth;
			imageTargetHeight = targetHeight;
		}
		else
		{
			throw new IllegalArgumentException( "Unsupported scale mode: " + scaleMode );
		}

		/*
		 * Choose interpolation method.
		 */
		final Object interpolation;
		if ( ( imageTargetWidth < scaledImageWidth ) || ( imageTargetHeight < scaledImageHeight ) )
		{
			interpolation = RenderingHints.VALUE_INTERPOLATION_BILINEAR;
		}
		else
		{
			interpolation = RenderingHints.VALUE_INTERPOLATION_BICUBIC;
		}

		/*
		 * Perform scaling, using multiple steps when reducing to less than 50%.
		 */
		BufferedImage result = source;
		while ( ( imageTargetWidth != scaledImageWidth ) || ( imageTargetHeight != scaledImageHeight ) )
		{
			scaledImageWidth = Math.max( scaledImageWidth / 2, imageTargetWidth );
			scaledImageHeight = Math.max( scaledImageHeight / 2, imageTargetHeight );

			/*
			 * Use specified target size instead of the aspect-correct size in
			 * the final scaling step.
			 */
			final boolean finalStep = ( scaledImageWidth == imageTargetWidth ) && ( scaledImageHeight == imageTargetHeight );
			final int scaledImageX = finalStep && haveBackground ? ( targetWidth - imageTargetWidth ) / 2 : 0;
			final int scaledImageY = finalStep && haveBackground ? ( targetHeight - imageTargetHeight ) / 2 : 0;
			final int scaledCanvasWidth = finalStep ? haveBackground ? targetWidth : imageTargetWidth : scaledImageWidth;
			final int scaledCanvasHeight = finalStep ? haveBackground ? targetHeight : imageTargetHeight : scaledImageHeight;
			/*
			 * Perform the scaling step.
			 */
			final BufferedImage scaledImage = new BufferedImage( scaledCanvasWidth, scaledCanvasHeight, scaledType );

			final Graphics2D g2 = scaledImage.createGraphics();
			if ( finalStep && haveBackground )
			{
				g2.setPaint( background );
				g2.fillRect( 0, 0, scaledCanvasWidth, scaledCanvasHeight );
			}
			g2.setRenderingHint( RenderingHints.KEY_INTERPOLATION, interpolation );
			g2.drawImage( result, scaledImageX, scaledImageY, haveBackground ? scaledImageWidth : scaledCanvasWidth, haveBackground ? scaledImageHeight : scaledCanvasHeight, null );
			g2.dispose();

			result = scaledImage;
		}

		return result;
	}

	/**
	 * Creates a grayscaled image from the given source. If no background color
	 * is specified, transparent areas of the source image will appear black in
	 * the result.
	 *
	 * @param source     Source image.
	 * @param background Background color; may be {@code null}.
	 *
	 * @return Grayscaled image.
	 */
	public static BufferedImage createGrayscaleInstance( final BufferedImage source, final Color background )
	{
		if ( source == null )
		{
			throw new NullPointerException( "source" );
		}

		if ( ( source.getWidth() <= 0 ) || ( source.getHeight() <= 0 ) )
		{
			throw new IllegalArgumentException( "source image has invalid size: " + source.getWidth() + " x " + source.getHeight() );
		}

		final int width = source.getWidth();
		final int height = source.getHeight();

		final BufferedImage grayscale = new BufferedImage( width, height, BufferedImage.TYPE_BYTE_GRAY );

		final Graphics2D g = grayscale.createGraphics();
		if ( background != null )
		{
			g.setColor( Color.WHITE );
			g.fillRect( 0, 0, width, height );
		}
		g.setRenderingHint( RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY );
		g.setRenderingHint( RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE );
		g.drawImage( source, 0, 0, null );
		g.dispose();
		return grayscale;
	}

	/**
	 * Bayer's optimal dither matrix.
	 *
	 * @see Halftoning
	 * Basics
	 */
	private static final int[][] BAYER_DITHER_MATRIX =
	{
	{ 1, 17, 5, 21, 2, 18, 6, 22 },
	{ 25, 9, 29, 13, 26, 10, 30, 14 },
	{ 7, 23, 3, 19, 8, 24, 4, 20 },
	{ 31, 15, 27, 11, 32, 16, 28, 12 },
	{ 2, 18, 6, 22, 1, 17, 5, 21 },
	{ 26, 10, 30, 14, 25, 9, 29, 13 },
	{ 8, 24, 4, 20, 7, 23, 3, 19 },
	{ 32, 16, 28, 12, 31, 15, 27, 11 }
	};

	/**
	 * Creates a halftoned black and white image from the given source. If no
	 * background color is specified, transparent areas of the source image will
	 * appear black in the result.
	 *
	 * @param source     Source image.
	 * @param background Background color; may be {@code null}.
	 *
	 * @return Black and white image.
	 *
	 * @see Image
	 * dithering using a dither matrix
	 */
	public static BufferedImage createBlackAndWhiteInstance( final BufferedImage source, final Color background )
	{
		if ( source == null )
		{
			throw new NullPointerException( "source" );
		}

		if ( ( source.getWidth() <= 0 ) || ( source.getHeight() <= 0 ) )
		{
			throw new IllegalArgumentException( "source image has invalid size: " + source.getWidth() + " x " + source.getHeight() );
		}

		/*
		 * For some reason Java2D doesn't perform dithering on 1-bit images,
		 * so we'll just do it ourselves.
		 */

		final int[][] ditherMatrix = BAYER_DITHER_MATRIX;

		final int width = source.getWidth();
		final int height = source.getHeight();
		final int n = ditherMatrix.length;

		final BufferedImage result = new BufferedImage( width, height, BufferedImage.TYPE_BYTE_BINARY );
		final WritableRaster resultRaster = result.getRaster();

		final boolean isIntRGB = ( source.getType() == BufferedImage.TYPE_INT_ARGB ) ||
		                         ( source.getType() == BufferedImage.TYPE_INT_RGB );

		final ColorModel colorModel = source.getColorModel();
		final boolean sourceHasAlpha = colorModel.hasAlpha();

		final WritableRaster sourceRaster = source.getRaster();
		final DataBuffer sourceDataBuffer = sourceRaster.getDataBuffer();

		final int backgroundRed = ( background == null ) ? 0 : background.getRed();
		final int backgroundGreen = ( background == null ) ? 0 : background.getGreen();
		final int backgroundBlue = ( background == null ) ? 0 : background.getBlue();

		for ( int y = 0; y < height; ++y )
		{
			for ( int x = 0; x < width; ++x )
			{
				/*
				 * Get source ARGB components for the pixel.
				 */
				final int argb = isIntRGB ? sourceDataBuffer.getElem( y * width + x ) : source.getRGB( x, y );
				int red = ( argb & 0xff0000 ) >>> 16;
				int green = ( argb & 0xff00 ) >>> 8;
				int blue = ( argb & 0xff );

				if ( sourceHasAlpha )
				{
					/*
					 * Apply background color.
					 */
					final int alpha = ( argb & 0xff000000 ) >>> 24;
					red = ( red * alpha + backgroundRed * ( 0xff - alpha ) ) / 0xff;
					green = ( green * alpha + backgroundGreen * ( 0xff - alpha ) ) / 0xff;
					blue = ( blue * alpha + backgroundBlue * ( 0xff - alpha ) ) / 0xff;
				}

				/*
				 * Grayscale and threshold according to dither matrix.
				 */
				final int gray = ( red * 30 + green * 59 + blue * 11 ) / 100;
				final int threshold = 0xff * ditherMatrix[ y % n ][ x % n ] / 32;
				if ( gray >= threshold )
				{
					resultRaster.setSample( x, y, 0, 1 );
				}
			}
		}

		return result;
	}

	/**
	 * Convenience method for writing image files.
	 *
	 * @param file     File to write image to.
	 * @param image    Image to write to file.
	 * @param mimeType Mime type to encode image with.
	 *
	 * @throws IOException if there was a problem writing the image file.
	 */
	public static void writeImage( final File file, final BufferedImage image, final String mimeType )
	throws IOException
	{
		if ( file == null )
		{
			throw new NullPointerException( "file" );
		}

		if ( image == null )
		{
			throw new NullPointerException( "image" );
		}

		if ( mimeType == null )
		{
			throw new NullPointerException( "mimeType" );
		}

		final ImageWriter imageWriter;
		{
			final Iterator writers = ImageIO.getImageWritersByMIMEType( mimeType );
			if ( !writers.hasNext() )
			{
				throw new IOException( "Unknown image MIME type '" + mimeType + '\'' );
			}

			imageWriter = writers.next();
		}

		final File parent = file.getParentFile();
		if ( parent != null )
		{
			parent.mkdirs();
		}

		final ImageOutputStream os = new FileImageOutputStream( file );
		try
		{
			imageWriter.setOutput( os );
			imageWriter.write( image );
		}
		finally
		{
			os.close();
		}
	}

	/**
	 * Writes the given image to the given file as a JPEG image. This method
	 * provides a convenient {@code quality} parameter to set the compression
	 * quality, which can be quite a hassle using the API directly.
	 *
	 * @param file    File to write to.
	 * @param image   Image to be written.
	 * @param quality Compression quality between 0 and 1.
	 *
	 * @throws IOException if an I/O error occurs.
	 */
	public static void writeImageAsJpeg( @NotNull final File file, @NotNull final BufferedImage image, final float quality )
	throws IOException
	{
		final Iterator imageWriters = ImageIO.getImageWritersByMIMEType( "image/jpeg" );
		if ( !imageWriters.hasNext() )
		{
			throw new IOException( "No image writers available for 'image/jpeg'." );
		}

		final ImageWriter imageWriter = imageWriters.next();
		final ImageWriteParam param = imageWriter.getDefaultWriteParam();
		param.setCompressionMode( ImageWriteParam.MODE_EXPLICIT );
		param.setCompressionQuality( quality );

		final FileImageOutputStream fileOut = new FileImageOutputStream( file );
		try
		{
			imageWriter.setOutput( fileOut );
			imageWriter.write( null, new IIOImage( image, null, null ), param );
			imageWriter.dispose();
		}
		finally
		{
			fileOut.close();
		}
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy