/**
* This file Copyright (c) 2012 Magnolia International
* Ltd. (http://www.magnolia-cms.com). All rights reserved.
*
*
* This file is dual-licensed under both the Magnolia
* Network Agreement and the GNU General Public License.
* You may elect to use one or the other of these licenses.
*
* This file is distributed in the hope that it will be
* useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
* implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
* Redistribution, except as permitted by whichever of the GPL
* or MNA you select, is prohibited.
*
* 1. For the GPL license (GPL), you can redistribute and/or
* modify this file under the terms of the GNU General
* Public License, Version 3, as published by the Free Software
* Foundation. You should have received a copy of the GNU
* General Public License, Version 3 along with this program;
* if not, write to the Free Software Foundation, Inc., 51
* Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* 2. For the Magnolia Network Agreement (MNA), this file
* and the accompanying materials are made available under the
* terms of the MNA which accompanies this distribution, and
* is available at http://www.magnolia-cms.com/mna.html
*
* Any modifications to this file must keep this entire header
* intact.
*
*/
package info.magnolia.templating.jsp.taglib;
import info.magnolia.cms.core.Content;
import info.magnolia.cms.core.HierarchyManager;
import info.magnolia.cms.core.ItemType;
import info.magnolia.cms.util.NodeDataUtil;
import info.magnolia.context.MgnlContext;
import info.magnolia.jcr.util.NodeTypes;
import info.magnolia.repository.RepositoryConstants;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Calendar;
import javax.imageio.ImageIO;
import javax.jcr.AccessDeniedException;
import javax.jcr.PathNotFoundException;
import javax.jcr.RepositoryException;
import javax.servlet.jsp.JspException;
import javax.servlet.jsp.JspWriter;
import org.slf4j.LoggerFactory;
import org.slf4j.Logger;
import org.tldgen.annotations.BodyContent;
import org.tldgen.annotations.Tag;
/**
* Creates a scaled copy of an image. The maximum width and height of the images can be specified via the
* attributes. <br />
* <br />
* If the scaled image with the specified name does not exist in the repository, then this tag will create it and save
* it. If the scaled image already exists, then it will not be recreated. <br />
* <br />
* The name of the node that contains the original image is set by the attribute 'parentContentNode', and the name of
* the nodeData for the image is set by the attribute 'parentNodeDataName'. If 'parentContentNode' is null, the local
* content node is used. <br />
* <br />
* The name of the content node that contains the new scaled image is set by the attribute 'imageContentNodeName'. This
* node is created under the original image node. This ensures that, if the original images is deleted, so are all the
* scaled versions. <br />
* <br />
* This tag writes out the handle of the content node that contains the image. <br />
* <br />
*
* @jsp.tag name="scaleImage" body-content="empty"
*
* @author Patrick Janssen
* @author Fabrizio Giustina
* @version 1.0
*/
@Tag(name="scaleImage", bodyContent=BodyContent.EMPTY)
public class ScaleImageTag extends BaseImageTag {
private static final Logger log = LoggerFactory.getLogger(ScaleImageTag.class);
/**
* Location for folder for temporary image creation.
*/
private static final String TEMP_IMAGE_NAME = "tmp-img";
/**
* The value of the extension nodeData in the properties node.
*/
private static final String PROPERTIES_EXTENSION_VALUE = "PNG";
/**
* Attribute: Image maximum height.
*/
private int maxHeight = 0;
/**
* Attribute: Image maximum width.
*/
private int maxWidth = 0;
/**
* Attribute: Allow resizing images beyond their original dimensions.
* Enabled by default for backwards compatibility but keep in mind this can
* result in images of very poor quality.
*/
private boolean allowOversize = true;
/**
* Attribute: The name of the new content node to create.
*/
private String imageContentNodeName;
/**
* Attribute: The name of the data node that contains the existing image.
*/
private String parentNodeDataName;
/**
* The maximum height of the image in pixels.
* @jsp.attribute required="false" rtexprvalue="true" type="int"
*/
public void setMaxHeight(int maxHeight) {
this.maxHeight = maxHeight;
}
/**
* The maximum width of the image in pixels.
* @jsp.attribute required="false" rtexprvalue="true" type="int"
*/
public void setMaxWidth(int maxWidth) {
this.maxWidth = maxWidth;
}
/**
* Allow resizing images beyond their original dimensions?
* @jsp.attribute required="false" rtexprvalue="true" type="boolean"
*/
public void setAllowOversize(boolean allowOversize) {
this.allowOversize = allowOversize;
}
/**
* The name of the content node that contains the image to be copied and scaled.
* @jsp.attribute required="false" rtexprvalue="true"
*/
@Override
public void setParentContentNodeName(String parentContentNodeName) {
this.parentContentNodeName = parentContentNodeName;
}
/**
* The name of the data node that contains the image data to be copied and scaled.
* @jsp.attribute required="true" rtexprvalue="true"
*/
public void setParentNodeDataName(String parentNodeDataName) {
this.parentNodeDataName = parentNodeDataName;
}
/**
* The name of the new contentNode that will contain the scaled version of the image.
* @jsp.attribute required="true" rtexprvalue="true"
*/
@Override
public void setImageContentNodeName(String imageContentNodeName) {
this.imageContentNodeName = imageContentNodeName;
}
@Override
public void doTag() throws JspException {
// initialize everything
Content parentContentNode;
Content imageContentNode;
JspWriter out = this.getJspContext().getOut();
try {
// set the parent node that contains the original image
if ((this.parentContentNodeName == null) || (this.parentContentNodeName.equals(""))) {
parentContentNode = MgnlContext.getAggregationState().getCurrentContent();
}
else {
HierarchyManager hm = MgnlContext.getHierarchyManager(RepositoryConstants.WEBSITE);
// if this name starts with a '/', then assume it is a node handle
// otherwise assume that its is a path relative to the local content node
if (this.parentContentNodeName.startsWith("/")) {
parentContentNode = hm.getContent(this.parentContentNodeName);
}
else {
String handle = MgnlContext.getAggregationState().getCurrentContent().getHandle();
parentContentNode = hm.getContent(handle + "/" + this.parentContentNodeName);
}
}
// check if the new image node exists, if not then create it
if (parentContentNode.hasContent(this.imageContentNodeName)) {
imageContentNode = parentContentNode.getContent(this.imageContentNodeName);
}
else {
// create the node
imageContentNode = parentContentNode.createContent(this.imageContentNodeName, ItemType.CONTENTNODE);
parentContentNode.save();
}
// if the node does not have the image data or should be rescaled (i.e., something has c
// then create the image data
if (!imageContentNode.hasNodeData(this.parentNodeDataName) || rescale(parentContentNode, imageContentNode)) {
this.createImageNodeData(parentContentNode, imageContentNode);
}
// write out the handle for the new image and exit
StringBuffer handle = new StringBuffer(imageContentNode.getHandle());
handle.append("/");
handle.append(getFilename());
out.write(handle.toString());
}
catch (PathNotFoundException e) {
log.error("PathNotFoundException occured in ScaleImage tag: " + e.getMessage(), e);
}
catch (AccessDeniedException e) {
log.error("AccessDeniedException occured in ScaleImage tag: " + e.getMessage(), e);
}
catch (RepositoryException e) {
log.error("RepositoryException occured in ScaleImage tag: " + e.getMessage(), e);
}
catch (FileNotFoundException e) {
log.error("FileNotFoundException occured in ScaleImage tag: " + e.getMessage(), e);
}
catch (IOException e) {
log.error("IOException occured in ScaleImage tag: " + e.getMessage(), e);
}
this.cleanUp();
}
/**
* Checks to see if the previously scaled image needs to be rescaled. This is true when the parent content node has
* been updated or the height or width parameters have changed.
* @param parentContentNode The node containing the scaled image node
* @param imageContentNode The scaled image node
* @return
*/
protected boolean rescale(Content parentContentNode, Content imageContentNode) {
try {
Calendar parentModified = NodeTypes.LastModified.getLastModified(parentContentNode.getJCRNode());
Calendar imageModified = NodeTypes.LastModified.getLastModified(imageContentNode.getJCRNode());
if (parentModified.after(imageModified)) {
return true;
}
} catch (RepositoryException e) {
// if we can't determine last modification of the nodes then do the safe thing and go for rescaling
return true;
}
int originalHeight = (int) imageContentNode.getNodeData("maxHeight").getLong();
int originalWidth = (int) imageContentNode.getNodeData("maxWidth").getLong();
return originalHeight != maxHeight || originalWidth != maxWidth;
}
/**
* Set objects to null.
*/
public void cleanUp() {
this.parentNodeDataName = null;
this.imageContentNodeName = null;
this.maxWidth = 0;
this.maxHeight = 0;
}
/**
* Create an image file that is a scaled version of the original image.
* @param imageContentNode node
*/
private void createImageNodeData(Content parentContentNode, Content imageContentNode) throws PathNotFoundException,
RepositoryException, IOException {
// get the original image, as a buffered image
InputStream oriImgStr = parentContentNode.getNodeData(this.parentNodeDataName).getStream();
BufferedImage oriImgBuff = ImageIO.read(oriImgStr);
oriImgStr.close();
// create the new image file
File newImgFile = this.scaleImage(oriImgBuff);
NodeDataUtil.getOrCreate(imageContentNode, "maxHeight").setValue(maxHeight);
NodeDataUtil.getOrCreate(imageContentNode, "maxWidth").setValue(maxWidth);
createImageNode(newImgFile, imageContentNode);
newImgFile.delete();
}
/**
* Create an image file that is a scaled version of the original image.
* @param oriImgBuff the original image file
* @return the new image file
*/
private File scaleImage(BufferedImage oriImgBuff) throws IOException {
// get the dimesnions of the original image
int oriWidth = oriImgBuff.getWidth();
int oriHeight = oriImgBuff.getHeight();
// get scale factor for the new image
double scaleFactor = this.scaleFactor(oriWidth, oriHeight);
// get the width and height of the new image
int newWidth = new Double(oriWidth * scaleFactor).intValue();
int newHeight = new Double(oriHeight * scaleFactor).intValue();
// create the thumbnail as a buffered image
Image newImg = oriImgBuff.getScaledInstance(newWidth, newHeight, Image.SCALE_AREA_AVERAGING);
BufferedImage newImgBuff = new BufferedImage(
newImg.getWidth(null),
newImg.getHeight(null),
BufferedImage.TYPE_INT_RGB);
Graphics2D g = newImgBuff.createGraphics();
g.drawImage(newImg, 0, 0, null);
g.dispose();
// create the new image file in the temporary dir
File newImgFile = File.createTempFile(TEMP_IMAGE_NAME, PROPERTIES_EXTENSION_VALUE);
ImageIO.write(newImgBuff, PROPERTIES_EXTENSION_VALUE, newImgFile);
// return the file
return newImgFile;
}
/**
* Calculate the scale factor for the image.
* @param originalWidth the image width
* @param originalHeight the image height
* @return the scale factor
*/
private double scaleFactor(int originalWidth, int originalHeight) {
double scaleFactor;
int scaleWidth = this.maxWidth;
int scaleHeight = this.maxHeight;
if (!this.allowOversize) {
scaleWidth = Math.min(this.maxWidth, originalWidth);
scaleHeight = Math.min(this.maxHeight, originalHeight);
}
if (scaleWidth <= 0 && scaleHeight <= 0) {
// may a copy at the same size
scaleFactor = 1;
}
else if (scaleWidth <= 0) {
// use height
scaleFactor = (double) scaleHeight / (double) originalHeight;
}
else if (scaleHeight <= 0) {
// use width
scaleFactor = (double) scaleWidth / (double) originalWidth;
}
else {
// create two scale factors, and see which is smaller
double scaleFactorWidth = (double) scaleWidth / (double) originalWidth;
double scaleFactorHeight = (double) scaleHeight / (double) originalHeight;
scaleFactor = Math.min(scaleFactorWidth, scaleFactorHeight);
}
return scaleFactor;
}
@Override
protected String getFilename() {
return this.parentNodeDataName;
}
}