/** * Copyright (C) 2011 JTalks.org Team * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ package org.jtalks.jcommune.service.nontransactional; import net.sf.image4j.codec.ico.ICODecoder; import net.sf.image4j.codec.ico.ICOEncoder; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.Validate; import org.apache.tika.Tika; import org.jtalks.jcommune.service.exceptions.ImageProcessException; import javax.imageio.ImageIO; import java.awt.*; import java.awt.image.BufferedImage; import java.io.*; /** * Class for converting image and saving it in the target format in the byte array. * Subclasses should define how to save image and what type target image will have * Some methods were taken from JForum: http://jforum.net/ * * @author Eugeny Batov * @author Alexandre Teterin * @author Andrei Alikov */ public class ImageConverter { /** * This prefix is used when specifying image as a byte array in SRC attribute * of IMG HTML tag. Used in AJAX avatar preview. */ protected static final String HTML_SRC_TAG_PREFIX = "data:image/%s;base64,"; private static final int ALPHA_CHANNEL_MASK = 0xFF000000; private static final int RED_CHANNEL_MASK = 0x00FF0000; private static final int GREEN_CHANNEL_MASK = 0x0000FF00; private static final int BLUE_CHANNEL_MASK = 0x000000FF; private static final int BIT = 8; private static final int TWO_BITS = BIT * 2; private static final int THREE_BITS = BIT * 3; private static final int ARGB_BITS_COUNT = BIT * 4; /** In some cases (e.g. {@link ICOEncoder write() method}) we can't work with images * having width < 8, default accessing kept for testing */ static final int MINIMUM_ICO_WIDTH = 8; private final Base64Wrapper base64Wrapper = new Base64Wrapper(); private final int maxImageWidth; private final int maxImageHeight; private final String format; private final int imageType; /** * @param format format of the target image * @param imageType image type of the target image (see {@link BufferedImage} documentation) * @param maxImageWidth maximum image width after pre processing * @param maxImageHeight maximum image height after pre processing */ public ImageConverter(String format, int imageType, int maxImageWidth, int maxImageHeight) { this.format = format; this.imageType = imageType; this.maxImageWidth = maxImageWidth; this.maxImageHeight = maxImageHeight; } /** * Gets target format of this converter * @return target image format */ public String getFormat() { return format; } /** * Gets prefix for "src" attribute of the "img" tag representing the image format * * @return prefix for "src" attribute of the "img" tag representing the image format */ public String getHtmlSrcImagePrefix() { return String.format(HTML_SRC_TAG_PREFIX, format); } /** * @param format format of the target image * @param maxImageWidth maximum image width after pre processing * @param maxImageHeight maximum image height after pre processing */ public static ImageConverter createConverter(String format, int maxImageWidth, int maxImageHeight) { int imageType = BufferedImage.TYPE_INT_ARGB; if (format.equals("jpeg")) { imageType = BufferedImage.TYPE_INT_RGB; } return new ImageConverter(format, imageType, maxImageWidth, maxImageHeight); } /** * Converts image to byte array. * * @param image input image, not null * @return byte array obtained from image * @throws ImageProcessException if an I/O error occurs */ public byte[] convertImageToByteArray(BufferedImage image) throws ImageProcessException { Validate.notNull(image, "Incoming image cannot be null"); byte[] result; ByteArrayOutputStream baos = new ByteArrayOutputStream(); try { if (format.equals("ico")) { ICOEncoder.write(image, ARGB_BITS_COUNT, baos); } else { ImageIO.write(image, format, baos); } baos.flush(); result = baos.toByteArray(); } catch (IOException e) { throw new ImageProcessException(e); } finally { IOUtils.closeQuietly(baos); } return result; } /** * Perform byte data conversion to BufferedImage. * * @param bytes for conversion. * @return image result. * @throws ImageProcessException image conversion problem. */ public BufferedImage convertByteArrayToImage(byte[] bytes) throws ImageProcessException { BufferedImage result; BufferedInputStream bis = new BufferedInputStream(new ByteArrayInputStream(bytes)); Tika tika = new Tika(); try { String type = tika.detect(bis); if (type.contains(ImageService.ICO_TYPE)) { result = ICODecoder.read(bis).get(0); } else { result = ImageIO.read(bis); } } catch (IOException | IndexOutOfBoundsException e) { throw new ImageProcessException(e); } finally { IOUtils.closeQuietly(bis); } return result; } /** * Resizes an image if its width or height is bigger than maximum value specified in the constructor or * smaller then minimum width value. * * @param image The image to resize * @param type int code jpeg, png or gif * @return A <code>BufferedImage</code> having width and height less or equal then maximum */ public BufferedImage resizeImage(BufferedImage image, int type) { Dimension largestDimension = new Dimension(maxImageWidth, maxImageHeight); // Original size int imageWidth = image.getWidth(null); int imageHeight = image.getHeight(null); float aspectRatio = (float) imageWidth / imageHeight; if (imageWidth > largestDimension.width || imageHeight > largestDimension.height) { if ((float) largestDimension.width / largestDimension.height > aspectRatio) { largestDimension.width = (int) Math.ceil(largestDimension.height * aspectRatio); } else { largestDimension.height = (int) Math.ceil(largestDimension.width / aspectRatio); } //Modified size imageWidth = largestDimension.width; imageHeight = largestDimension.height; } if (imageWidth < MINIMUM_ICO_WIDTH && format.equals("ico")) { aspectRatio = (float) MINIMUM_ICO_WIDTH / (float)imageWidth; imageWidth = MINIMUM_ICO_WIDTH; imageHeight *= aspectRatio; // if we can't resize image properly (with same aspect ratio, but having width >= minimum width) - just // decrease image height if (imageHeight > maxImageHeight) { imageHeight = maxImageHeight; } } return createBufferedImage(image, type, imageWidth, imageHeight); } /** * Perform image resizing and processing * * @param image for processing * @return processed image bytes * @throws ImageProcessException image processing problem */ public byte[] preprocessImage(BufferedImage image) throws ImageProcessException { byte[] result; BufferedImage outputImage = resizeImage(image, imageType); result = convertImageToByteArray(outputImage); return result; } /** * Uploaded bytes are converted into base64 string, * so that it can be passed via HTTP protocol and form HTML page. * * @param avatar bytes of the uploaded image * @return encoded image in base64 */ public String prepareHtmlImgSrc(byte[] avatar) { return base64Wrapper.encodeB64Bytes(avatar); } /** * Creates a <code>BufferedImage</code> from an <code>Image</code>. This method can * function on a completely headless system. This especially includes Linux and Unix systems * that do not have the X11 libraries installed, which are required for the AWT subsystem to * operate. The resulting image will be smoothly scaled using bilinear filtering. * * @param source The image to convert * @param width The desired image width * @param height The desired image height * @param imageType int code RGB or ARGB * @return bufferedImage The resized image */ private BufferedImage createBufferedImage(BufferedImage source, int imageType, int width, int height) { BufferedImage bufferedImage = new BufferedImage(width, height, imageType); int sourceX; int sourceY; double scaleX = (double) width / source.getWidth(); double scaleY = (double) height / source.getHeight(); int x1; int y1; double xDiff; double yDiff; int rgb; int rgb1; int rgb2; for (int y = 0; y < height; y++) { sourceY = y * source.getHeight() / bufferedImage.getHeight(); yDiff = y / scaleY - sourceY; for (int x = 0; x < width; x++) { sourceX = x * source.getWidth() / bufferedImage.getWidth(); xDiff = x / scaleX - sourceX; x1 = Math.min(source.getWidth() - 1, sourceX + 1); y1 = Math.min(source.getHeight() - 1, sourceY + 1); rgb1 = getRGBInterpolation(source.getRGB(sourceX, sourceY), source.getRGB(x1, sourceY), xDiff); rgb2 = getRGBInterpolation(source.getRGB(sourceX, y1), source.getRGB(x1, y1), xDiff); rgb = getRGBInterpolation(rgb1, rgb2, yDiff); bufferedImage.setRGB(x, y, rgb); } } return bufferedImage; } /** * Makes rgb interpolation. * * @param value1 first known value * @param value2 second known value * @param distance distance between values * @return rgb an integer pixel in the ARGB color model */ private int getRGBInterpolation(int value1, int value2, double distance) { int alpha1 = (value1 & ALPHA_CHANNEL_MASK) >>> THREE_BITS; int red1 = (value1 & RED_CHANNEL_MASK) >> TWO_BITS; int green1 = (value1 & GREEN_CHANNEL_MASK) >> BIT; int blue1 = (value1 & BLUE_CHANNEL_MASK); int alpha2 = (value2 & ALPHA_CHANNEL_MASK) >>> THREE_BITS; int red2 = (value2 & RED_CHANNEL_MASK) >> TWO_BITS; int green2 = (value2 & GREEN_CHANNEL_MASK) >> BIT; int blue2 = (value2 & BLUE_CHANNEL_MASK); return ((int) (alpha1 * (1.0 - distance) + alpha2 * distance) << THREE_BITS) | ((int) (red1 * (1.0 - distance) + red2 * distance) << TWO_BITS) | ((int) (green1 * (1.0 - distance) + green2 * distance) << BIT) | (int) (blue1 * (1.0 - distance) + blue2 * distance); } }