/*
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
* This 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 software 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 software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package com.celements.photo.image;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.awt.image.DataBuffer;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
import javax.imageio.ImageIO;
import javax.media.jai.PixelAccessor;
import javax.media.jai.UnpackedImageData;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.celements.photo.container.ImageDimensions;
import com.celements.photo.container.ImageLibStrings;
import com.celements.photo.utilities.Util;
import com.xpn.xwiki.XWikiException;
/**
* This class is used to generate a thumbnail. Until now it only works for jpeg
* files.
*/
public class GenerateThumbnail {
private static final Log mLogger = LogFactory.getFactory().getInstance(GenerateThumbnail.class);
/**
* saveTypes : image types which can be preserved resizing the image
*/
public static final Map<String, String> saveTypes = getSaveTypes();
static Map<String, String> getSaveTypes() {
HashMap<String, String> map = new HashMap<String, String>();
// map.put("gif", "GIF");
// map.put("image/gif", "GIF");
// map.put("jpg", "JPEG");
// map.put("jpeg", "JPEG");
// map.put("image/jpeg", "JPEG");
map.put("png", "PNG");
map.put("image/png", "PNG");
return map;
}
public GenerateThumbnail(){}
/**
* Calculates an ImageDimensions object, containing the width and height of a
* thumbnail, respecting the specified maximums (maintaining the aspect
* ratio of the original image).
*
* @see com.celements.photo.plugin.container.ImageDimensions
* @param in InputStream of the image.
* @param maxWidth Maximum allowed width.
* @param maxHeight Maximum allowed height.
* @return ImageDimensions object specifying width and height.
* @throws XWikiException
*/
public ImageDimensions getThumbnailDimensions(InputStream in, int maxWidth, int maxHeight) throws XWikiException{
BufferedImage img = decodeImage(in);
return getThumbnailDimensions(img, maxWidth, maxHeight);
}
/**
* Calculates an ImageDimensions object, containing the width and height of a
* thumbnail, respecting the specified maximums (maintaining the aspect
* ratio of the original image).
*
* @see com.celements.photo.plugin.container.ImageDimensions
* @param img BufferedImage representation of the image.
* @param maxWidth Maximum allowed width.
* @param maxHeight Maximum allowed height.
* @return ImageDimensions object specifying width and height.
*/
public ImageDimensions getThumbnailDimensions(BufferedImage img, int maxWidth, int maxHeight){
int width = maxWidth;
int height = maxHeight;
if(img != null) {
width = img.getWidth();
height = img.getHeight();
}
mLogger.debug("img width=" + width + "; img height=" + height);
return getThumbnailDimensions(width, height, maxWidth, maxHeight);
}
/**
* Calculates an ImageDimensions object, containing the width and height of a
* thumbnail, respecting the specified maximums (maintaining the aspect
* ratio of the original image).
* Maximum values <= 0 for a dimension take the original image's value as
* maximum for that dimension.
*
* @see com.celements.photo.plugin.container.ImageDimensions
* @param imgWidth Width of the original image.
* @param imgHeight Height of the original image.
* @param maxWidth Maximum allowed width.
* @param maxHeight Maximum allowed height.
* @return ImageDimensions object specifying width and height.
*
* TODO implement preserving aspect ratio!!! see commented test in GenerateThumbnailTest
*/
public ImageDimensions getThumbnailDimensions(int imgWidth, int imgHeight, int maxWidth,
int maxHeight) {
int thumbWidth = imgWidth;
int thumbHeight = imgHeight;
if(maxWidth <= 0){ maxWidth = imgWidth; }
if(maxHeight <= 0){ maxHeight = imgHeight; }
double widthImgThumbRatio = imgWidth / (double)maxWidth;
double heightImgThumbRatio = imgHeight / (double)maxHeight;
if((widthImgThumbRatio >= 1.0) || (heightImgThumbRatio >= 1.0)){
if(widthImgThumbRatio > heightImgThumbRatio){
thumbWidth = maxWidth;
thumbHeight = (int) Math.ceil(imgHeight / widthImgThumbRatio);
} else{
thumbHeight = maxHeight;
thumbWidth = (int)Math.ceil(imgWidth / heightImgThumbRatio);
}
}
return new ImageDimensions(thumbWidth, thumbHeight);
}
/**
* Calculates the dimensions of the specified image.
*
* @see com.celements.photo.plugin.container.ImageDimensions
* @param in InputStream of the image.
* @return ImageDimensions object containing width and height of the image.
* @throws XWikiException
*/
public ImageDimensions getImageDimensions(InputStream in) throws XWikiException{
BufferedImage img = decodeImage(in);
if (img != null) {
return new ImageDimensions(img.getWidth(), img.getHeight());
}
return null;
}
/**
* Calculates the dimensions of the specified image.
*
* @see com.celements.photo.plugin.container.ImageDimensions
* @param img BufferedImage representation of the image.
* @return ImageDimensions object containing width and height of the image.
*/
public ImageDimensions getImageDimensions(BufferedImage img){
return new ImageDimensions(img.getWidth(), img.getHeight());
}
/**
* Decodes an InputStream of a jpg image and returns a BufferedImage.
*
* @param in InputStream of the image.
* @return BufferedImage representation of the image.
* @throws XWikiException
*/
public BufferedImage decodeInputStream(InputStream in) throws XWikiException{
return decodeImage(in);
}
/**
* Creates a thumbnail, reading from an InputStream and writing the result
* to an OutputStream.
*
* @param in InputStream of the original sized image.
* @param out OutputStream of the thumbnail.
* @param width Maximum width of the thumbnail (Aspect ratio maintained).
* @param height Maximum height of the thumbnail (aspect ratio maintained).
* @param watermark String to add as a watermark to the image.
* @param copyright String to add as a copyright to the image.
* @return An ImageSize object representing the size of the thumbnail.
* @throws IOException
* @throws XWikiException
*/
public ImageDimensions createThumbnail(InputStream in, OutputStream out, int width,
int height, String watermark, String copyright, String type, Color defaultBg)
throws IOException, XWikiException {
return createThumbnail(decodeInputStream(in), out, width, height, watermark,
copyright, type, defaultBg);
}
/**
* Creates a thumbnail, reading from an InputStream and writing the result
* to an OutputStream.
*
* @param in InputStream of the original sized image.
* @param out OutputStream of the thumbnail.
* @param dimensions Dimensions of the thumbnail (Aspect ratio maintained).
* @param watermark String to add as a watermark to the image.
* @param copyright String to add as a copyright to the image.
* @return An ImageSize object representing the size of the thumbnail.
* @throws IOException
* @throws XWikiException
*/
public void createThumbnail(InputStream in, OutputStream out,
ImageDimensions dimensions, String watermark, String copyright, String type,
Color defaultBg) throws IOException, XWikiException {
createThumbnail(decodeInputStream(in), out, dimensions, watermark, copyright, type,
defaultBg);
}
/**
* Creates a thumbnail from a BufferedImage and writes the result
* to an OutputStream.
*
* @param img BufferedImage representation of the original image.
* @param out OutputStream of the thumbnail.
* @param width Maximum width of the thumbnail (Aspect ratio maintained).
* @param height Maximum height of the thumbnail (aspect ratio maintained).
* @param watermark String to add as a watermark to the image.
* @param copyright String to add as a copyright to the image.
* @return An ImageSize object representing the size of the thumbnail.
* @throws IOException
*/
public ImageDimensions createThumbnail(BufferedImage img, OutputStream out, int width,
int height, String watermark, String copyright, String type, Color defaultBg)
throws IOException {
ImageDimensions imgSize = getThumbnailDimensions(img, width, height);
createThumbnail(img, out, imgSize, watermark, copyright, type, defaultBg);
return imgSize;
}
public BufferedImage createThumbnail(BufferedImage img, OutputStream out,
ImageDimensions imgSize, String watermark, String copyright, String type,
Color defaultBg) {
Image thumbImg = img;
// Only generates a thumbnail if the image is larger than the desired thumbnail.
mLogger.debug("img: " + img + " - imgSize: " + imgSize);
if((img.getWidth() > (int)imgSize.getWidth()) || (img.getHeight() > (int)imgSize.getHeight())){
// The "-1" is used to resize maintaining the aspect ratio.
thumbImg = img.getScaledInstance((int)imgSize.getWidth(), -1, Image.SCALE_SMOOTH);
}
mLogger.debug("width ziel: " + imgSize.getWidth() + ", height ziel: " +
imgSize.getHeight() + "; width: " + thumbImg.getWidth(null) + ", height: " +
thumbImg.getHeight(null));
BufferedImage buffThumb = convertImageToBufferedImage(thumbImg, watermark, copyright,
defaultBg);
encodeImage(out, buffThumb, img, type);
return buffThumb;
}
/**
* Encodes a BufferedImage to jpeg format and writes it to the specified
* OutputStream.
*
* @param out OutputStream to write the image to.
* @param image BufferedImage of the image to encode.
* @throws IOException
*/
public void encodeImage(OutputStream out, BufferedImage image, BufferedImage fallback,
String type) {
if(!saveTypes.containsKey(type.toLowerCase())) {
mLogger.info("encodeImage: convert to png, because [" + type + "] is no saveType.");
type = "png"; //default for all not jpeg or gif files
}
try {
ImageIO.write(image, saveTypes.get(type.toLowerCase()), out);
} catch (IOException ioe) {
mLogger.error("Could not save image as [" + type + "]! " + ioe);
try {
ImageIO.write(fallback, saveTypes.get(type.toLowerCase()), out);
} catch (IOException e) {
mLogger.error("Could not save fallback image as [" + type + "]! " + e);
}
}
}
/*
* Converts an Image to a BufferedImage and adds watermark and copyright.
*
* @param thumbImg The Image to convert.
* @param watermark String to add as a watermark to the image.
* @param copyright String to add as a copyright to the image.
* @return The BufferedImage representation of the Image.
*/
BufferedImage convertImageToBufferedImage(Image thumbImg, String watermark,
String copyright, Color defaultBg) {
BufferedImage thumb = new BufferedImage(thumbImg.getWidth(null),
thumbImg.getHeight(null), BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = thumb.createGraphics();
if(defaultBg != null) {
g2d.setColor(defaultBg);
g2d.fillRect(0, 0, thumbImg.getWidth(null), thumbImg.getHeight(null));
}
g2d.drawImage(thumbImg, 0, 0, null);
if((watermark != null) && (!watermark.equals(""))){
drawWatermark(watermark, g2d, thumb.getWidth(), thumb.getHeight());
}
if((copyright != null) && (!copyright.equals(""))){
drawCopyright(copyright, g2d, thumb.getWidth(), thumb.getHeight());
}
mLogger.info("thumbDimensions: " + thumb.getHeight() + "x" + thumb.getWidth());
return thumb;
}
/*
* Draws the watermark onto the image.
*
* @param watermark String to draw on the image.
* @param g2d Graphics object of the image.
*/
private void drawWatermark(String watermark, Graphics2D g2d, int width, int height) {
//TODO implement 'secure' watermark (i.e. check if this algorithm secure or can it be easily reversed?)
g2d.setColor(new Color(255, 255, 150));
AlphaComposite transprency = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.45f);
g2d.setComposite(transprency);
FontMetrics metrics = calcWatermarkFontSize(watermark, width, g2d);
g2d.setFont(calcWatermarkFontSize(watermark, width, g2d).getFont());
// rotate around image center
double angle = (Math.sin(height/(double)width));
g2d.rotate(-angle, width / 2.0, height / 2.0);
g2d.drawString(
watermark,
(width - metrics.stringWidth(watermark)) / 2,
((height - metrics.getHeight()) / 2) + metrics.getAscent());
// undo rotation for correct copyright positioning
g2d.rotate(angle, width / 2.0, height / 2.0);
}
private FontMetrics calcWatermarkFontSize(String watermark, int width,
Graphics2D g2d) {
FontMetrics metrics;
int fontSize = 1;
do{
metrics = g2d.getFontMetrics(new Font(g2d.getFont().getFontName(), Font.BOLD, fontSize));
fontSize++;
}while(metrics.stringWidth(watermark) < (0.8*width));
return metrics;
}
/*
* Draws the copyright information onto the image.
*
* @param copyright String to draw on the image.
* @param g2d Graphics object of the image.
*/
private void drawCopyright(String copyright, Graphics2D g2d, int width, int height) {
int bottomSpace = 5; //space between copyright and bottom border.
int rightSpace = 5; //space between copyright and right border.
int hSpacing = 3; //horizontal space between background and string.
int vSpacing = 2; //vertical space between background and string.
int rounding = 5; //rounding of the rect.
FontMetrics metrics = calcCopyrightFontSize(copyright, width, g2d);
g2d.setFont(metrics.getFont());
int stringHeight = metrics.getHeight();
drawBackground(copyright, width, height, bottomSpace, rightSpace, vSpacing,
hSpacing, rounding, stringHeight, metrics, g2d);
drawString(copyright, width, height, bottomSpace, rightSpace, vSpacing,
hSpacing, metrics, g2d);
}
private FontMetrics calcCopyrightFontSize(String copyright, int width, Graphics2D g2d) {
FontMetrics metrics;
int fontSize = 16;
do{
metrics = g2d.getFontMetrics(new Font(g2d.getFont().getFontName(), Font.BOLD, fontSize));
fontSize--;
}while((fontSize > 0) && (metrics.stringWidth(copyright) > (width/3)));
return metrics;
}
private void drawBackground(String copyright, int width, int height,
int bottomSpace, int rightSpace, int vSpacing, int hSpacing,
int rounding, int stringHeight, FontMetrics metrics, Graphics2D g2d) {
g2d.setColor(new Color(0, 0, 0));
AlphaComposite transprency = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.4f);
g2d.setComposite(transprency);
g2d.fillRoundRect(
width - metrics.stringWidth(copyright) - rightSpace - 2*hSpacing,
height - stringHeight - bottomSpace - 2*vSpacing,
metrics.stringWidth(copyright) + 2*hSpacing,
stringHeight + 2*vSpacing,
rounding, rounding);
}
private void drawString(String copyright, int width, int height,
int bottomSpace, int rightSpace, int vSpacing, int hSpacing,
FontMetrics metrics, Graphics2D g2d) {
AlphaComposite transprency;
g2d.setColor(new Color(255, 255, 255));
transprency = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.66f);
g2d.setComposite(transprency);
//Attention: drawString's [x, y] coordinates are the baseline, not upper or lower corner.
g2d.drawString(
copyright,
width - metrics.stringWidth(copyright) - hSpacing - rightSpace,
height - metrics.getDescent() - vSpacing - bottomSpace);
}
/**
* Decodes a jpeg from an InputStream to a BufferedImage.
*
* Replaced by DecodeImageCommand which can handle CMYK images and embedded profiles.
*
* @param in InputStream representation of a jpeg image.
* @return Decoded jpeg as BufferedImage.
* @throws XWikiException
*/
@Deprecated
public BufferedImage decodeImage(InputStream in) throws XWikiException {
BufferedImage bufferedImage = null;
try {
bufferedImage = ImageIO.read(in);
} catch (IOException e) {
mLogger.error("Could not open the file! ", e);
throw new XWikiException(XWikiException.MODULE_XWIKI_PLUGINS,
XWikiException.ERROR_XWIKI_UNKNOWN, "Could not decode the image file.", e);
}
return bufferedImage;
}
/**
* Get a hash code for an image. Only the image data is used in the
* calculation, the metainformation is excluded.
* The return value is formated as string of the hex codes of the original
* hash code. Thus, each hash code character is represented by two
* characters, representing a hex code. This is done to be able to use the
* hash in file paths, URLs, ...
*
* The hashing is done using the SHA-256 algorithm. Thus the resulting hash
* has a length of 256 bit (32 byte) i.e. after the hex conversion 64 byte.
*
* @param in InputStream with the image to hash.
* @return Hash code of the specified image, excluding metainformation.
* @throws IOException
* @throws XWikiException
*/
public String hashImage(InputStream in) throws IOException, XWikiException{
BufferedImage img = decodeImage(in);
String hash = "";
try {
hash = Util.getUtil().hashToHex(getHashOfImage(img));
} catch (NoSuchAlgorithmException e) {
mLogger.error(e);
}
return hash;
}
private String getHashOfImage(BufferedImage img)
throws NoSuchAlgorithmException {
MessageDigest digest = MessageDigest.getInstance(ImageLibStrings.HASHING_ALGORITHM);
PixelAccessor pa = new PixelAccessor(img);
UnpackedImageData uid = pa.getPixels(img.getData(), img.getData().getBounds(),
DataBuffer.TYPE_BYTE, false);
byte[][] pixels = uid.getByteData();
for (int i = 0; i < pixels.length; i++) {
digest.update(pixels[i]);
}
return new String(digest.digest());
}
}