/* * Jitsi, the OpenSource Java VoIP and Instant Messaging client. * * Copyright @ 2015 Atlassian Pty Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.java.sip.communicator.plugin.desktoputil; import java.awt.*; import java.awt.image.*; import java.io.*; import java.net.*; import javax.imageio.*; import javax.swing.*; import net.java.sip.communicator.util.*; /** * Utility methods for image manipulation. * * @author Sebastien Mazy * @author Yana Stamcheva * @author Lubomir Marinov */ public class ImageUtils { /** * The Logger used by the ImageUtils class for logging * output. */ private static final Logger logger = Logger.getLogger(ImageUtils.class); /** * Different shapes that an image can be cropped to. */ private static enum Shape { /** * Ellipse with the same height and width as the scaled image (this * will be a circle if the un-cropped image is square). */ ELLIPSE, /** * Rectangle with the corners rounded to arcs with radius 3 pixels */ ROUNDED_RECTANGLE; } /** * Returns a scaled image fitting within the given bounds while keeping the * aspect ratio. * * @param image the image to scale * @param width maximum width of the scaled image * @param height maximum height of the scaled image * @return the scaled image */ public static Image scaleImageWithinBounds( Image image, int width, int height) { int initialWidth = image.getWidth(null); int initialHeight = image.getHeight(null); Image scaledImage; int scaleHint = Image.SCALE_SMOOTH; double originalRatio = (double) initialWidth / initialHeight; double areaRatio = (double) width / height; if(originalRatio > areaRatio) scaledImage = image.getScaledInstance(width, -1, scaleHint); else scaledImage = image.getScaledInstance(-1, height, scaleHint); return scaledImage; } /** * Scales the given image to fit in the given width and * height. * @param image the image to scale * @param width the desired width * @param height the desired height * @return the scaled image */ public static ImageIcon scaleIconWithinBounds(Image image, int width, int height) { return new ImageIcon(scaleImageWithinBounds(image, width, height)); } /** * Scales the given image to fit in the given width and * height. * * @param imageBytes the bytes of the image to scale * @param width the desired width * @param height the desired height * @return the scaled image */ public static ImageIcon scaleImageWithinBounds( byte[] imageBytes, int width, int height) { if (imageBytes == null || !(imageBytes.length > 0)) return null; Image imageIcon = null; try { Image image; // sometimes ImageIO fails, will fall back to awt Toolkit try { image = ImageIO.read(new ByteArrayInputStream(imageBytes)); } catch (Exception e) { try { image = Toolkit.getDefaultToolkit().createImage(imageBytes); } catch (Exception e1) { // if it fails throw the original exception throw e; } } if(image != null) imageIcon = scaleImageWithinBounds(image, width, height); else if (logger.isTraceEnabled()) logger.trace("Unknown image format or error reading image"); } catch (Exception e) { if (logger.isDebugEnabled()) logger.debug("Could not create image.", e); } if (imageIcon != null) return new ImageIcon(imageIcon); return null; } /** * Creates a rounded avatar image. * * @param image image of the initial avatar image. * @param width the desired width * @param height the desired height * @return The rounded corner image. */ public static Image getScaledRoundedImage( Image image, int width, int height) { return getScaledImage(image, Shape.ROUNDED_RECTANGLE, width, height); } /** * Creates a elliptical avatar image. * * @param image image of the initial avatar image. * @param width the desired width * @param height the desired height * @return The elliptical image. */ public static Image getScaledEllipticalImage( Image image, int width, int height) { return getScaledImage(image, Shape.ELLIPSE, width, height); } /** * Creates an avatar image in the specified shape. * * @param image image of the initial avatar image. * @param shape the desired shape * @param width the desired width * @param height the desired height * @return The cropped, scaled image. */ private static Image getScaledImage( Image image, Shape shape, int width, int height) { ImageIcon scaledImage = ImageUtils.scaleIconWithinBounds(image, width, height); int scaledImageWidth = scaledImage.getIconWidth(); int scaledImageHeight = scaledImage.getIconHeight(); if(scaledImageHeight <= 0 || scaledImageWidth <= 0) return null; // Just clipping the image would cause jaggies on Windows and Linux. // The following is a soft clipping solution based on the solution // proposed by Chris Campbell: // http://java.sun.com/mailers/techtips/corejava/2006/tt0923.html BufferedImage destImage = new BufferedImage(scaledImageWidth, scaledImageHeight, BufferedImage.TYPE_INT_ARGB); Graphics2D g = destImage.createGraphics(); try { // Render our clip shape into the image. Note that we enable // antialiasing to achieve the soft clipping effect. g.setComposite(AlphaComposite.Src); AntialiasingManager.activateAntialiasing(g); g.setColor(Color.WHITE); switch (shape) { case ELLIPSE: g.fillOval(0, 0, scaledImageWidth, scaledImageHeight); break; case ROUNDED_RECTANGLE: g.fillRoundRect(0, 0, scaledImageWidth, scaledImageHeight, 5, 5); break; } // We use SrcAtop, which effectively uses the // alpha value as a coverage value for each pixel stored in the // destination. For the areas outside our clip shape, the // destination alpha will be zero, so nothing is rendered in those // areas. For the areas inside our clip shape, the destination alpha // will be fully opaque, so the full color is rendered. At the edges, // the original antialiasing is carried over to give us the desired // soft clipping effect. g.setComposite(AlphaComposite.SrcAtop); g.drawImage(scaledImage.getImage(), 0, 0, null); } finally { g.dispose(); } return destImage; } /** * Returns a scaled rounded instance of the given image. * @param image the image to scale * @param width the desired width * @param height the desired height * @return a byte array containing the scaled rounded image */ public static byte[] getScaledInstanceInBytes( Image image, int width, int height) { BufferedImage scaledImage = (BufferedImage) getScaledRoundedImage(image, width, height); return convertImageToBytes(scaledImage); } /** * Returns a scaled elliptical instance of the given image. * @param image the image to scale * @param width the desired width * @param height the desired height * @return a byte array containing the scaled elliptical image */ public static byte[] getScaledEllipticalInstanceInBytes( Image image, int width, int height) { BufferedImage scaledImage = (BufferedImage) getScaledEllipticalImage(image, width, height); return convertImageToBytes(scaledImage); } /** * Returns a scaled rounded icon from the given image, scaled * within the given width and height. * @param image the image to scale * @param width the maximum width of the scaled icon * @param height the maximum height of the scaled icon * @return a scaled rounded icon */ public static ImageIcon getScaledRoundedIcon(Image image, int width, int height) { Image scaledImage = getScaledRoundedImage(image, width, height); if (scaledImage != null) return new ImageIcon(scaledImage); return null; } /** * Returns a scaled elliptical icon from the given image, scaled * within the given width and height. * @param image the image to scale * @param width the maximum width of the scaled icon * @param height the maximum height of the scaled icon * @return a scaled elliptical icon */ public static ImageIcon getScaledEllipticalIcon(Image image, int width, int height) { Image scaledImage = getScaledEllipticalImage(image, width, height); if (scaledImage != null) return new ImageIcon(scaledImage); return null; } /** * Creates a rounded corner scaled image. * * @param imageBytes The bytes of the image to be scaled. * @param width The maximum width of the scaled image. * @param height The maximum height of the scaled image. * * @return The rounded corner scaled image. */ public static ImageIcon getScaledRoundedIcon( byte[] imageBytes, int width, int height) { return getScaledIcon(imageBytes, Shape.ROUNDED_RECTANGLE, width, height); } /** * Creates a elliptical scaled image. * * @param imageBytes The bytes of the image to be scaled. * @param width The maximum width of the scaled image. * @param height The maximum height of the scaled image. * * @return The elliptical scaled image. */ public static ImageIcon getScaledEllipticalIcon( byte[] imageBytes, int width, int height) { return getScaledIcon(imageBytes, Shape.ELLIPSE, width, height); } /** * Creates a cropped, scaled image. * * @param imageBytes The bytes of the image to be scaled. * @param shape The shape of the scaled image. * @param width The maximum width of the scaled image. * @param height The maximum height of the scaled image. * * @return The cropped, scaled image. */ private static ImageIcon getScaledIcon( byte[] imageBytes, Shape shape, int width, int height) { if (imageBytes == null || !(imageBytes.length > 0)) return null; ImageIcon imageIcon = null; try { Image image; // sometimes ImageIO fails, will fall back to awt Toolkit try { image = ImageIO.read(new ByteArrayInputStream(imageBytes)); } catch (Exception e) { try { image = Toolkit.getDefaultToolkit().createImage(imageBytes); } catch (Exception e1) { // if it fails throw the original exception throw e; } } if(image != null) { switch (shape) { case ELLIPSE: imageIcon = getScaledEllipticalIcon(image, width, height); break; case ROUNDED_RECTANGLE: imageIcon = getScaledRoundedIcon(image, width, height); break; } } else if (logger.isTraceEnabled()) logger.trace("Unknown image format or error reading image"); } catch (Exception e) { if (logger.isDebugEnabled()) logger.debug("Could not create image.", e); } return imageIcon; } /** * Returns the buffered image corresponding to the given url image path. * * @param imagePath the path indicating, where we can find the image. * * @return the buffered image corresponding to the given url image path. */ public static BufferedImage getBufferedImage(URL imagePath) { BufferedImage image = null; if (imagePath != null) { try { image = ImageIO.read(imagePath); } catch (IOException ex) { if (logger.isDebugEnabled()) logger.debug("Failed to load image:" + imagePath, ex); } } return image; } /** * Returns the buffered image corresponding to the given image * @param source an image * @return the buffered image corresponding to the given image */ public static BufferedImage getBufferedImage(Image source) { if (source == null) { return null; } else if (source instanceof BufferedImage) { return (BufferedImage) source; } int width = source.getWidth(null); int height = source.getHeight(null); BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); Graphics graphics = image.createGraphics(); graphics.drawImage(source, 0, 0, null); graphics.dispose(); return image; } /** * Extracts bytes from image. * @param image the image. * @return the bytes of the image. */ public static byte[] toByteArray(BufferedImage image) { ByteArrayOutputStream out = new ByteArrayOutputStream(); try { ImageIO.write(image, "png", out); } catch (IOException e) { logger.debug("Cannot convert buffered image to byte[]", e); return null; } return out.toByteArray(); } /** * Loads an image from a given bytes array. * @param imageBytes The bytes array to load the image from. * @return The image for the given bytes array. */ public static Image getBytesInImage(byte[] imageBytes) { if (imageBytes == null || !(imageBytes.length > 0)) return null; Image image = null; try { image = ImageIO.read(new ByteArrayInputStream(imageBytes)); } catch (Exception e) { logger.error("Failed to convert bytes to image.", e); } return image; } /** * Creates a composed image from two images. If one of the images * is missing will add an empty space on its place. * @param leftImage the left image. * @param rightImage the right image * @param imageObserver need to calculate image sizes. * @return the composed image. */ public static Image getComposedImage( Image leftImage, Image rightImage, ImageObserver imageObserver) { int height; int leftWidth; int width; if (leftImage == null) { if (rightImage == null) { return null; } else { height = rightImage.getHeight(imageObserver); leftWidth = rightImage.getWidth(imageObserver); width = leftWidth * 2; } } else if (rightImage == null) { height = leftImage.getHeight(imageObserver); leftWidth = leftImage.getWidth(imageObserver); width = leftWidth * 2; } else { height = Math.max( leftImage.getHeight(imageObserver), rightImage.getHeight(imageObserver)); leftWidth = leftImage.getWidth(imageObserver); width = leftWidth + rightImage.getWidth(imageObserver); } BufferedImage buffImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); Graphics2D g = (Graphics2D) buffImage.getGraphics(); AntialiasingManager.activateAntialiasing(g); g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER)); if (leftImage != null) g.drawImage(leftImage, 0, 0, null); if (rightImage != null) g.drawImage(rightImage, leftWidth + 1, 0, null); return buffImage; } /** * Returns a bytes of the given scaledImage. * @param scaledImage the image to scale * @return a byte array containing the scaled image */ private static byte[] convertImageToBytes(BufferedImage scaledImage) { byte[] scaledBytes = null; if (scaledImage != null) { ByteArrayOutputStream outStream = new ByteArrayOutputStream(); try { ImageIO.write(scaledImage, "png", outStream); scaledBytes = outStream.toByteArray(); } catch (IOException e) { if (logger.isDebugEnabled()) logger.debug("Could not scale image in bytes.", e); } } return scaledBytes; } /** * Sets the image of the incoming call notification. * * @param label the label to set the image to * @param image the image to set * @param width the desired image width * @param height the desired image height */ public static void setScaledLabelImage( final JLabel label, final byte[] image, final int width, final int height) { if(!SwingUtilities.isEventDispatchThread()) { SwingUtilities.invokeLater(new Runnable() { public void run() { setScaledLabelImage(label, image, width, height); } }); return; } label.setIcon(getScaledRoundedIcon(image, width, height)); label.repaint(); } }