/** * Copyright (c) 2000-present Liferay, Inc. All rights reserved. * * 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. */ package com.liferay.portal.image; import com.liferay.portal.kernel.concurrent.FutureConverter; import com.liferay.portal.kernel.exception.ImageResolutionException; import com.liferay.portal.kernel.image.ImageBag; import com.liferay.portal.kernel.image.ImageMagick; import com.liferay.portal.kernel.image.ImageTool; import com.liferay.portal.kernel.io.unsync.UnsyncByteArrayOutputStream; import com.liferay.portal.kernel.log.Log; import com.liferay.portal.kernel.log.LogFactoryUtil; import com.liferay.portal.kernel.model.Image; import com.liferay.portal.kernel.security.pacl.DoPrivileged; import com.liferay.portal.kernel.util.ArrayUtil; import com.liferay.portal.kernel.util.CharPool; import com.liferay.portal.kernel.util.GetterUtil; import com.liferay.portal.kernel.util.PropsKeys; import com.liferay.portal.kernel.util.StringBundler; import com.liferay.portal.kernel.util.StringUtil; import com.liferay.portal.model.impl.ImageImpl; import com.liferay.portal.module.framework.ModuleFrameworkUtilAdapter; import com.liferay.portal.util.FileImpl; import com.liferay.portal.util.PropsUtil; import com.liferay.portal.util.PropsValues; import java.awt.AlphaComposite; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.geom.AffineTransform; import java.awt.image.AffineTransformOp; import java.awt.image.BufferedImage; import java.awt.image.ColorModel; import java.awt.image.DataBuffer; import java.awt.image.IndexColorModel; import java.awt.image.RenderedImage; import java.awt.image.SampleModel; import java.awt.image.WritableRaster; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URL; import java.util.Arrays; import java.util.Hashtable; import java.util.Iterator; import java.util.LinkedList; import java.util.Queue; import java.util.concurrent.Future; import javax.imageio.ImageIO; import javax.imageio.ImageReader; import javax.imageio.spi.IIORegistry; import javax.imageio.spi.ImageReaderSpi; import javax.imageio.stream.ImageInputStream; import net.jmge.gif.Gif89Encoder; import org.im4java.core.IMOperation; import org.monte.media.jpeg.CMYKJPEGImageReaderSpi; /** * @author Brian Wing Shun Chan * @author Alexander Chow * @author Shuyang Zhou */ @DoPrivileged public class ImageToolImpl implements ImageTool { public static ImageTool getInstance() { return _instance; } public void afterPropertiesSet() { Class<?> clazz = getClass(); ClassLoader classLoader = clazz.getClassLoader(); try { InputStream inputStream = classLoader.getResourceAsStream( PropsUtil.get(PropsKeys.IMAGE_DEFAULT_SPACER)); if (inputStream == null) { _log.error("Default spacer is not available"); } _defaultSpacer = getImage(inputStream); } catch (Exception e) { _log.error( "Unable to configure the default spacer: " + e.getMessage()); } try { InputStream inputStream = null; String imageDefaultCompanyLogo = PropsUtil.get( PropsKeys.IMAGE_DEFAULT_COMPANY_LOGO); int index = imageDefaultCompanyLogo.indexOf(CharPool.SEMICOLON); if (index == -1) { inputStream = classLoader.getResourceAsStream( PropsUtil.get(PropsKeys.IMAGE_DEFAULT_COMPANY_LOGO)); } else { String bundleIdString = imageDefaultCompanyLogo.substring( 0, index); int bundleId = GetterUtil.getInteger(bundleIdString, -1); String name = imageDefaultCompanyLogo.substring(index + 1); if (bundleId < 0) { if (_log.isWarnEnabled()) { _log.warn( "Fallback to portal class loader because of " + "invalid bundle ID " + bundleIdString); } inputStream = classLoader.getResourceAsStream(name); } else { URL url = ModuleFrameworkUtilAdapter.getBundleResource( bundleId, name); inputStream = url.openStream(); } } if (inputStream == null) { _log.error("Default company logo is not available"); } _defaultCompanyLogo = getImage(inputStream); } catch (Exception e) { _log.error( "Unable to configure the default company logo: " + e.getMessage()); } try { InputStream is = classLoader.getResourceAsStream( PropsUtil.get(PropsKeys.IMAGE_DEFAULT_ORGANIZATION_LOGO)); if (is == null) { _log.error("Default organization logo is not available"); } _defaultOrganizationLogo = getImage(is); } catch (Exception e) { _log.error( "Unable to configure the default organization logo: " + e.getMessage()); } try { InputStream is = classLoader.getResourceAsStream( PropsUtil.get(PropsKeys.IMAGE_DEFAULT_USER_FEMALE_PORTRAIT)); if (is == null) { _log.error("Default user female portrait is not available"); } _defaultUserFemalePortrait = getImage(is); } catch (Exception e) { _log.error( "Unable to configure the default user female portrait: " + e.getMessage()); } try { InputStream is = classLoader.getResourceAsStream( PropsUtil.get(PropsKeys.IMAGE_DEFAULT_USER_MALE_PORTRAIT)); if (is == null) { _log.error("Default user male portrait is not available"); } _defaultUserMalePortrait = getImage(is); } catch (Exception e) { _log.error( "Unable to configure the default user male portrait: " + e.getMessage()); } } @Override public Future<RenderedImage> convertCMYKtoRGB( byte[] bytes, final String type) { ImageMagick imageMagick = getImageMagick(); if (!imageMagick.isEnabled()) { return null; } File inputFile = _fileUtil.createTempFile(type); final File outputFile = _fileUtil.createTempFile(type); try { _fileUtil.write(inputFile, bytes); IMOperation imOperation = new IMOperation(); imOperation.addRawArgs("-format", "%[colorspace]"); imOperation.addImage(inputFile.getPath()); String[] output = imageMagick.identify(imOperation.getCmdArgs()); if ((output.length == 1) && StringUtil.equalsIgnoreCase(output[0], "CMYK")) { if (_log.isInfoEnabled()) { _log.info("The image is in the CMYK colorspace"); } imOperation = new IMOperation(); imOperation.addRawArgs("-colorspace", "RGB"); imOperation.addImage(inputFile.getPath()); imOperation.addImage(outputFile.getPath()); Future<Object> future = (Future<Object>)imageMagick.convert( imOperation.getCmdArgs()); return new FutureConverter<RenderedImage, Object>(future) { @Override protected RenderedImage convert(Object obj) { RenderedImage renderedImage = null; try { ImageBag imageBag = read( _fileUtil.getBytes(outputFile)); renderedImage = imageBag.getRenderedImage(); } catch (ImageResolutionException | IOException e) { if (_log.isDebugEnabled()) { _log.debug("Unable to convert " + type, e); } } return renderedImage; } }; } } catch (Exception e) { _log.error(e, e); } finally { _fileUtil.delete(inputFile); _fileUtil.delete(outputFile); } return null; } @Override public BufferedImage convertImageType(BufferedImage sourceImage, int type) { BufferedImage targetImage = new BufferedImage( sourceImage.getWidth(), sourceImage.getHeight(), type); Graphics2D graphics2D = targetImage.createGraphics(); graphics2D.drawRenderedImage(sourceImage, null); graphics2D.dispose(); return targetImage; } @Override public RenderedImage crop( RenderedImage renderedImage, int height, int width, int x, int y) { Rectangle rectangle = new Rectangle(x, y, width, height); Rectangle croppedRectangle = rectangle.intersection( new Rectangle(renderedImage.getWidth(), renderedImage.getHeight())); BufferedImage bufferedImage = getBufferedImage(renderedImage); return bufferedImage.getSubimage( croppedRectangle.x, croppedRectangle.y, croppedRectangle.width, croppedRectangle.height); } @Override public void encodeGIF(RenderedImage renderedImage, OutputStream os) throws IOException { BufferedImage bufferedImage = getBufferedImage(renderedImage); if (!(bufferedImage.getColorModel() instanceof IndexColorModel)) { bufferedImage = convertImageType( bufferedImage, BufferedImage.TYPE_BYTE_INDEXED); } Gif89Encoder encoder = new Gif89Encoder(bufferedImage); encoder.encode(os); } @Override public void encodeWBMP(RenderedImage renderedImage, OutputStream os) throws IOException { BufferedImage bufferedImage = getBufferedImage(renderedImage); SampleModel sampleModel = bufferedImage.getSampleModel(); int type = sampleModel.getDataType(); if ((bufferedImage.getType() != BufferedImage.TYPE_BYTE_BINARY) || (type < DataBuffer.TYPE_BYTE) || (type > DataBuffer.TYPE_INT) || (sampleModel.getNumBands() != 1) || (sampleModel.getSampleSize(0) != 1)) { BufferedImage binaryImage = new BufferedImage( bufferedImage.getWidth(), bufferedImage.getHeight(), BufferedImage.TYPE_BYTE_BINARY); Graphics graphics = binaryImage.getGraphics(); graphics.drawImage(bufferedImage, 0, 0, null); renderedImage = binaryImage; } if (!ImageIO.write(renderedImage, "wbmp", os)) { // See http://www.jguru.com/faq/view.jsp?EID=127723 os.write(0); os.write(0); os.write(toMultiByte(bufferedImage.getWidth())); os.write(toMultiByte(bufferedImage.getHeight())); DataBuffer dataBuffer = bufferedImage.getData().getDataBuffer(); int size = dataBuffer.getSize(); for (int i = 0; i < size; i++) { os.write((byte)dataBuffer.getElem(i)); } } } @Override public RenderedImage flipHorizontal(RenderedImage renderedImage) { BufferedImage bufferedImage = getBufferedImage(renderedImage); AffineTransform affineTransform = AffineTransform.getScaleInstance( -1.0, 1.0); affineTransform.translate(-bufferedImage.getWidth(), 0); AffineTransformOp affineTransformOp = new AffineTransformOp( affineTransform, null); return affineTransformOp.filter(bufferedImage, null); } @Override public RenderedImage flipVertical(RenderedImage renderedImage) { BufferedImage bufferedImage = getBufferedImage(renderedImage); AffineTransform affineTransform = AffineTransform.getScaleInstance( 1.0, -1.0); affineTransform.translate(0, -bufferedImage.getHeight()); AffineTransformOp affineTransformOp = new AffineTransformOp( affineTransform, null); return affineTransformOp.filter(bufferedImage, null); } @Override public BufferedImage getBufferedImage(RenderedImage renderedImage) { if (renderedImage instanceof BufferedImage) { return (BufferedImage)renderedImage; } ColorModel colorModel = renderedImage.getColorModel(); WritableRaster writableRaster = colorModel.createCompatibleWritableRaster( renderedImage.getWidth(), renderedImage.getHeight()); Hashtable<String, Object> properties = new Hashtable<>(); String[] keys = renderedImage.getPropertyNames(); if (!ArrayUtil.isEmpty(keys)) { for (String key : keys) { properties.put(key, renderedImage.getProperty(key)); } } BufferedImage bufferedImage = new BufferedImage( colorModel, writableRaster, colorModel.isAlphaPremultiplied(), properties); renderedImage.copyData(writableRaster); return bufferedImage; } @Override public byte[] getBytes(RenderedImage renderedImage, String contentType) throws IOException { UnsyncByteArrayOutputStream baos = new UnsyncByteArrayOutputStream(); write(renderedImage, contentType, baos); return baos.toByteArray(); } @Override public Image getDefaultCompanyLogo() { return _defaultCompanyLogo; } @Override public Image getDefaultOrganizationLogo() { return _defaultOrganizationLogo; } @Override public Image getDefaultSpacer() { return _defaultSpacer; } @Override public Image getDefaultUserFemalePortrait() { return _defaultUserFemalePortrait; } @Override public Image getDefaultUserMalePortrait() { return _defaultUserMalePortrait; } @Override public Image getImage(byte[] bytes) throws ImageResolutionException, IOException { if (bytes == null) { return null; } ImageBag imageBag = read(bytes); RenderedImage renderedImage = imageBag.getRenderedImage(); if (renderedImage == null) { throw new IOException("Unable to decode image"); } String type = imageBag.getType(); int height = renderedImage.getHeight(); int width = renderedImage.getWidth(); int size = bytes.length; Image image = new ImageImpl(); image.setTextObj(bytes); image.setType(type); image.setHeight(height); image.setWidth(width); image.setSize(size); return image; } @Override public Image getImage(File file) throws ImageResolutionException, IOException { byte[] bytes = _fileUtil.getBytes(file); return getImage(bytes); } @Override public Image getImage(InputStream is) throws ImageResolutionException, IOException { byte[] bytes = _fileUtil.getBytes(is, -1, true); return getImage(bytes); } @Override public Image getImage(InputStream is, boolean cleanUpStream) throws ImageResolutionException, IOException { byte[] bytes = _fileUtil.getBytes(is, -1, cleanUpStream); return getImage(bytes); } @Override public boolean isNullOrDefaultSpacer(byte[] bytes) { if (ArrayUtil.isEmpty(bytes) || Arrays.equals(bytes, getDefaultSpacer().getTextObj())) { return true; } else { return false; } } @Override public ImageBag read(byte[] bytes) throws ImageResolutionException, IOException { String formatName = null; ImageInputStream imageInputStream = null; Queue<ImageReader> imageReaders = new LinkedList<>(); RenderedImage renderedImage = null; try { imageInputStream = ImageIO.createImageInputStream( new ByteArrayInputStream(bytes)); Iterator<ImageReader> iterator = ImageIO.getImageReaders( imageInputStream); while ((renderedImage == null) && iterator.hasNext()) { ImageReader imageReader = iterator.next(); imageReaders.offer(imageReader); try { imageReader.setInput(imageInputStream); int height = imageReader.getHeight(0); int width = imageReader.getWidth(0); if (((PropsValues.IMAGE_TOOL_IMAGE_MAX_HEIGHT > 0) && (height > PropsValues.IMAGE_TOOL_IMAGE_MAX_HEIGHT)) || ((PropsValues.IMAGE_TOOL_IMAGE_MAX_WIDTH > 0) && (width > PropsValues.IMAGE_TOOL_IMAGE_MAX_WIDTH))) { StringBundler sb = new StringBundler(9); sb.append("Image's dimensions ("); sb.append(height); sb.append(" px high and "); sb.append(width); sb.append(" px wide) exceed max dimensions ("); sb.append(PropsValues.IMAGE_TOOL_IMAGE_MAX_HEIGHT); sb.append(" px high and "); sb.append(PropsValues.IMAGE_TOOL_IMAGE_MAX_WIDTH); sb.append(" px wide)"); throw new ImageResolutionException(sb.toString()); } renderedImage = imageReader.read(0); } catch (IOException ioe) { continue; } formatName = StringUtil.toLowerCase( imageReader.getFormatName()); } if (renderedImage == null) { throw new IOException("Unsupported image type"); } } finally { while (!imageReaders.isEmpty()) { ImageReader imageReader = imageReaders.poll(); imageReader.dispose(); } if (imageInputStream != null) { imageInputStream.close(); } } String type = TYPE_JPEG; if (formatName.contains(TYPE_BMP)) { type = TYPE_BMP; } else if (formatName.contains(TYPE_GIF)) { type = TYPE_GIF; } else if (formatName.contains("jpeg") || StringUtil.equalsIgnoreCase(type, "jpeg")) { type = TYPE_JPEG; } else if (formatName.contains(TYPE_PNG)) { type = TYPE_PNG; } else if (formatName.contains(TYPE_TIFF)) { type = TYPE_TIFF; } else { throw new IllegalArgumentException(type + " is not supported"); } return new ImageBag(renderedImage, type); } @Override public ImageBag read(File file) throws ImageResolutionException, IOException { return read(_fileUtil.getBytes(file)); } @Override public ImageBag read(InputStream inputStream) throws ImageResolutionException, IOException { return read(_fileUtil.getBytes(inputStream)); } @Override public RenderedImage rotate(RenderedImage renderedImage, int degrees) { BufferedImage bufferedImage = getBufferedImage(renderedImage); int imageWidth = bufferedImage.getWidth(); int imageHeight = bufferedImage.getHeight(); double radians = Math.toRadians(degrees); double absoluteSin = Math.abs(Math.sin(radians)); double absoluteCos = Math.abs(Math.cos(radians)); int rotatedImageWidth = (int)Math.floor( (imageWidth * absoluteCos) + (imageHeight * absoluteSin)); int rotatedImageHeight = (int)Math.floor( (imageHeight * absoluteCos) + (imageWidth * absoluteSin)); BufferedImage rotatedBufferedImage = new BufferedImage( rotatedImageWidth, rotatedImageHeight, bufferedImage.getType()); AffineTransform affineTransform = new AffineTransform(); affineTransform.translate( rotatedImageWidth / 2, rotatedImageHeight / 2); affineTransform.rotate(radians); affineTransform.translate(imageWidth / (-2), imageHeight / (-2)); Graphics2D graphics2D = rotatedBufferedImage.createGraphics(); graphics2D.drawImage(bufferedImage, affineTransform, null); graphics2D.dispose(); return rotatedBufferedImage; } @Override public RenderedImage scale(RenderedImage renderedImage, int width) { if (width <= 0) { return renderedImage; } int imageHeight = renderedImage.getHeight(); int imageWidth = renderedImage.getWidth(); double factor = (double)width / imageWidth; int scaledHeight = (int)Math.round(factor * imageHeight); int scaledWidth = width; return doScale(renderedImage, scaledHeight, scaledWidth); } @Override public RenderedImage scale( RenderedImage renderedImage, int maxHeight, int maxWidth) { int imageHeight = renderedImage.getHeight(); int imageWidth = renderedImage.getWidth(); if (maxHeight == 0) { maxHeight = imageHeight; } if (maxWidth == 0) { maxWidth = imageWidth; } if ((imageHeight <= maxHeight) && (imageWidth <= maxWidth)) { return renderedImage; } double factor = Math.min( (double)maxHeight / imageHeight, (double)maxWidth / imageWidth); int scaledHeight = Math.max(1, (int)Math.round(factor * imageHeight)); int scaledWidth = Math.max(1, (int)Math.round(factor * imageWidth)); return doScale(renderedImage, scaledHeight, scaledWidth); } @Override public void write( RenderedImage renderedImage, String contentType, OutputStream os) throws IOException { if (contentType.contains(TYPE_BMP)) { ImageIO.write(renderedImage, "bmp", os); } else if (contentType.contains(TYPE_GIF)) { encodeGIF(renderedImage, os); } else if (contentType.contains(TYPE_JPEG) || contentType.contains("jpeg")) { ImageIO.write(renderedImage, "jpeg", os); } else if (contentType.contains(TYPE_PNG)) { ImageIO.write(renderedImage, TYPE_PNG, os); } else if (contentType.contains(TYPE_TIFF) || contentType.contains("tif")) { ImageIO.write(renderedImage, "tiff", os); } } protected RenderedImage doScale( RenderedImage renderedImage, int scaledHeight, int scaledWidth) { // See http://www.oracle.com/technetwork/java/index-137037.html BufferedImage originalBufferedImage = getBufferedImage(renderedImage); BufferedImage scaledBufferedImage = new BufferedImage( scaledWidth, scaledHeight, originalBufferedImage.getType()); int originalHeight = originalBufferedImage.getHeight(); int originalWidth = originalBufferedImage.getWidth(); if (((scaledHeight * 2) >= originalHeight) && ((scaledWidth * 2) >= originalWidth)) { Graphics2D scaledGraphics2D = scaledBufferedImage.createGraphics(); scaledGraphics2D.drawImage( originalBufferedImage, 0, 0, scaledWidth, scaledHeight, null); scaledGraphics2D.dispose(); return scaledBufferedImage; } BufferedImage tempBufferedImage = new BufferedImage( originalWidth, originalHeight, scaledBufferedImage.getType()); Graphics2D tempGraphics2D = tempBufferedImage.createGraphics(); RenderingHints renderingHints = new RenderingHints( RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); tempGraphics2D.setRenderingHints(renderingHints); ColorModel originalColorModel = originalBufferedImage.getColorModel(); if (originalColorModel.hasAlpha()) { tempGraphics2D.setComposite(AlphaComposite.Src); } int startHeight = scaledHeight; int startWidth = scaledWidth; while ((startHeight < originalHeight) && (startWidth < originalWidth)) { startHeight *= 2; startWidth *= 2; } originalHeight = startHeight / 2; originalWidth = startWidth / 2; tempGraphics2D.drawImage( originalBufferedImage, 0, 0, originalWidth, originalHeight, null); while ((originalHeight >= (scaledHeight * 2)) && (originalWidth >= (scaledWidth * 2))) { originalHeight /= 2; if (originalHeight < scaledHeight) { originalHeight = scaledHeight; } originalWidth /= 2; if (originalWidth < scaledWidth) { originalWidth = scaledWidth; } tempGraphics2D.drawImage( tempBufferedImage, 0, 0, originalWidth, originalHeight, 0, 0, originalWidth * 2, originalHeight * 2, null); } tempGraphics2D.dispose(); Graphics2D scaledGraphics2D = scaledBufferedImage.createGraphics(); scaledGraphics2D.drawImage( tempBufferedImage, 0, 0, scaledWidth, scaledHeight, 0, 0, originalWidth, originalHeight, null); scaledGraphics2D.dispose(); return scaledBufferedImage; } protected ImageMagick getImageMagick() { if (_imageMagick == null) { _imageMagick = ImageMagickImpl.getInstance(); _imageMagick.reset(); } return _imageMagick; } protected void orderImageReaderSpis() { IIORegistry defaultIIORegistry = IIORegistry.getDefaultInstance(); ImageReaderSpi firstImageReaderSpi = null; ImageReaderSpi secondImageReaderSpi = null; Iterator<ImageReaderSpi> imageReaderSpis = defaultIIORegistry.getServiceProviders(ImageReaderSpi.class, true); while (imageReaderSpis.hasNext()) { ImageReaderSpi imageReaderSpi = imageReaderSpis.next(); if (imageReaderSpi instanceof CMYKJPEGImageReaderSpi) { secondImageReaderSpi = imageReaderSpi; } else { String[] formatNames = imageReaderSpi.getFormatNames(); if (ArrayUtil.contains(formatNames, TYPE_JPEG, true) || ArrayUtil.contains(formatNames, "jpeg", true)) { firstImageReaderSpi = imageReaderSpi; } } } if ((firstImageReaderSpi != null) && (secondImageReaderSpi != null)) { defaultIIORegistry.setOrdering( ImageReaderSpi.class, firstImageReaderSpi, secondImageReaderSpi); } } protected byte[] toMultiByte(int intValue) { int numBits = 32; int mask = 0x80000000; while ((mask != 0) && ((intValue & mask) == 0)) { numBits--; mask >>>= 1; } int numBitsLeft = numBits; byte[] multiBytes = new byte[(numBitsLeft + 6) / 7]; int maxIndex = multiBytes.length - 1; for (int b = 0; b <= maxIndex; b++) { multiBytes[b] = (byte)((intValue >>> ((maxIndex - b) * 7)) & 0x7f); if (b != maxIndex) { multiBytes[b] |= (byte)0x80; } } return multiBytes; } private ImageToolImpl() { ImageIO.setUseCache(PropsValues.IMAGE_IO_USE_DISK_CACHE); orderImageReaderSpis(); } private static final Log _log = LogFactoryUtil.getLog(ImageToolImpl.class); private static final ImageTool _instance = new ImageToolImpl(); private static final FileImpl _fileUtil = FileImpl.getInstance(); private static ImageMagick _imageMagick; private Image _defaultCompanyLogo; private Image _defaultOrganizationLogo; private Image _defaultSpacer; private Image _defaultUserFemalePortrait; private Image _defaultUserMalePortrait; }