View Javadoc
1   /**
2    * This file Copyright (c) 2012-2018 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.templating.jsp.taglib;
35  
36  import info.magnolia.cms.core.Content;
37  import info.magnolia.cms.core.HierarchyManager;
38  import info.magnolia.cms.core.ItemType;
39  import info.magnolia.cms.util.NodeDataUtil;
40  import info.magnolia.context.MgnlContext;
41  import info.magnolia.jcr.util.NodeTypes;
42  import info.magnolia.repository.RepositoryConstants;
43  
44  import java.awt.Graphics2D;
45  import java.awt.Image;
46  import java.awt.image.BufferedImage;
47  import java.io.File;
48  import java.io.FileNotFoundException;
49  import java.io.IOException;
50  import java.io.InputStream;
51  import java.util.Calendar;
52  
53  import javax.imageio.ImageIO;
54  import javax.jcr.AccessDeniedException;
55  import javax.jcr.PathNotFoundException;
56  import javax.jcr.RepositoryException;
57  import javax.servlet.jsp.JspException;
58  import javax.servlet.jsp.JspWriter;
59  
60  import org.slf4j.Logger;
61  import org.slf4j.LoggerFactory;
62  import org.tldgen.annotations.BodyContent;
63  import org.tldgen.annotations.Tag;
64  
65  /**
66   * Creates a scaled copy of an image. The maximum width and height of the images can be specified via the
67   * attributes. <br />
68   * <br />
69   * If the scaled image with the specified name does not exist in the repository, then this tag will create it and save
70   * it. If the scaled image already exists, then it will not be recreated. <br />
71   * <br />
72   * The name of the node that contains the original image is set by the attribute 'parentContentNode', and the name of
73   * the nodeData for the image is set by the attribute 'parentNodeDataName'. If 'parentContentNode' is null, the local
74   * content node is used. <br />
75   * <br />
76   * The name of the content node that contains the new scaled image is set by the attribute 'imageContentNodeName'. This
77   * node is created under the original image node. This ensures that, if the original images is deleted, so are all the
78   * scaled versions. <br />
79   * <br />
80   * This tag writes out the handle of the content node that contains the image. <br />
81   * <br />
82   *
83   * @jsp.tag name="scaleImage" body-content="empty"
84   */
85  @Tag(name = "scaleImage", bodyContent = BodyContent.EMPTY)
86  
87  public class ScaleImageTag extends BaseImageTag {
88      private static final Logger log = LoggerFactory.getLogger(ScaleImageTag.class);
89  
90      /**
91       * Location for folder for temporary image creation.
92       */
93      private static final String TEMP_IMAGE_NAME = "tmp-img";
94  
95      /**
96       * The value of the extension nodeData in the properties node.
97       */
98      private static final String PROPERTIES_EXTENSION_VALUE = "PNG";
99  
100     /**
101      * Attribute: Image maximum height.
102      */
103     private int maxHeight = 0;
104 
105     /**
106      * Attribute: Image maximum width.
107      */
108     private int maxWidth = 0;
109 
110     /**
111      * Attribute: Allow resizing images beyond their original dimensions.
112      * Enabled by default for backwards compatibility but keep in mind this can
113      * result in images of very poor quality.
114      */
115     private boolean allowOversize = true;
116 
117     /**
118      * Attribute: The name of the new content node to create.
119      */
120     private String imageContentNodeName;
121 
122     /**
123      * Attribute: The name of the data node that contains the existing image.
124      */
125     private String parentNodeDataName;
126 
127     /**
128      * The maximum height of the image in pixels.
129      *
130      * @jsp.attribute required="false" rtexprvalue="true" type="int"
131      */
132     public void setMaxHeight(int maxHeight) {
133         this.maxHeight = maxHeight;
134     }
135 
136     /**
137      * The maximum width of the image in pixels.
138      *
139      * @jsp.attribute required="false" rtexprvalue="true" type="int"
140      */
141     public void setMaxWidth(int maxWidth) {
142         this.maxWidth = maxWidth;
143     }
144 
145     /**
146      * Allow resizing images beyond their original dimensions?
147      *
148      * @jsp.attribute required="false" rtexprvalue="true" type="boolean"
149      */
150     public void setAllowOversize(boolean allowOversize) {
151         this.allowOversize = allowOversize;
152     }
153 
154     /**
155      * The name of the content node that contains the image to be copied and scaled.
156      *
157      * @jsp.attribute required="false" rtexprvalue="true"
158      */
159     @Override
160     public void setParentContentNodeName(String parentContentNodeName) {
161         this.parentContentNodeName = parentContentNodeName;
162     }
163 
164     /**
165      * The name of the data node that contains the image data to be copied and scaled.
166      *
167      * @jsp.attribute required="true" rtexprvalue="true"
168      */
169     public void setParentNodeDataName(String parentNodeDataName) {
170         this.parentNodeDataName = parentNodeDataName;
171     }
172 
173     /**
174      * The name of the new contentNode that will contain the scaled version of the image.
175      *
176      * @jsp.attribute required="true" rtexprvalue="true"
177      */
178     @Override
179     public void setImageContentNodeName(String imageContentNodeName) {
180         this.imageContentNodeName = imageContentNodeName;
181     }
182 
183     @Override
184     public void doTag() throws JspException {
185         // initialize everything
186         Content parentContentNode;
187         Content imageContentNode;
188         JspWriter out = this.getJspContext().getOut();
189 
190         try {
191 
192             // set the parent node that contains the original image
193             if ((this.parentContentNodeName == null) || (this.parentContentNodeName.equals(""))) {
194                 parentContentNode = MgnlContext.getAggregationState().getCurrentContent();
195             } else {
196                 HierarchyManager hm = MgnlContext.getHierarchyManager(RepositoryConstants.WEBSITE);
197                 // if this name starts with a '/', then assume it is a node handle
198                 // otherwise assume that its is a path relative to the local content node
199                 if (this.parentContentNodeName.startsWith("/")) {
200                     parentContentNode = hm.getContent(this.parentContentNodeName);
201                 } else {
202                     String handle = MgnlContext.getAggregationState().getCurrentContentNode().getPath();
203                     parentContentNode = hm.getContent(handle + "/" + this.parentContentNodeName);
204                 }
205             }
206 
207             // check if the new image node exists, if not then create it
208             if (parentContentNode.hasContent(this.imageContentNodeName)) {
209                 imageContentNode = parentContentNode.getContent(this.imageContentNodeName);
210             } else {
211                 // create the node
212                 imageContentNode = parentContentNode.createContent(this.imageContentNodeName, ItemType.CONTENTNODE);
213                 parentContentNode.save();
214             }
215             // if the node does not have the image data or should be rescaled (i.e., something has c
216             // then create the image data
217             if (!imageContentNode.hasNodeData(this.parentNodeDataName) || rescale(parentContentNode, imageContentNode)) {
218                 this.createImageNodeData(parentContentNode, imageContentNode);
219             }
220             // write out the handle for the new image and exit
221             StringBuffer handle = new StringBuffer(imageContentNode.getHandle());
222             handle.append("/");
223             handle.append(getFilename());
224             out.write(handle.toString());
225         } catch (PathNotFoundException e) {
226             log.error("PathNotFoundException occured in ScaleImage tag: {}", e.getMessage(), e);
227         } catch (AccessDeniedException e) {
228             log.error("AccessDeniedException occured in ScaleImage tag: {}", e.getMessage(), e);
229         } catch (RepositoryException e) {
230             log.error("RepositoryException occured in ScaleImage tag: {}", e.getMessage(), e);
231         } catch (FileNotFoundException e) {
232             log.error("FileNotFoundException occured in ScaleImage tag: {}", e.getMessage(), e);
233         } catch (IOException e) {
234             log.error("IOException occured in ScaleImage tag: {}", e.getMessage(), e);
235         }
236         this.cleanUp();
237     }
238 
239     /**
240      * Checks to see if the previously scaled image needs to be rescaled. This is true when the parent content node has
241      * been updated or the height or width parameters have changed.
242      *
243      * @param parentContentNode The node containing the scaled image node
244      * @param imageContentNode The scaled image node
245      */
246     protected boolean rescale(Content parentContentNode, Content imageContentNode) {
247 
248         try {
249             Calendar parentModified = NodeTypes.LastModified.getLastModified(parentContentNode.getJCRNode());
250             Calendar imageModified = NodeTypes.LastModified.getLastModified(imageContentNode.getJCRNode());
251 
252             if (parentModified.after(imageModified)) {
253                 return true;
254             }
255         } catch (RepositoryException e) {
256             // if we can't determine last modification of the nodes then do the safe thing and go for rescaling
257             return true;
258         }
259 
260         int originalHeight = (int) imageContentNode.getNodeData("maxHeight").getLong();
261         int originalWidth = (int) imageContentNode.getNodeData("maxWidth").getLong();
262 
263         return originalHeight != maxHeight || originalWidth != maxWidth;
264     }
265 
266     /**
267      * Set objects to null.
268      */
269     public void cleanUp() {
270         this.parentNodeDataName = null;
271         this.imageContentNodeName = null;
272         this.maxWidth = 0;
273         this.maxHeight = 0;
274     }
275 
276     /**
277      * Create an image file that is a scaled version of the original image.
278      *
279      * @param imageContentNode node
280      */
281     private void createImageNodeData(Content parentContentNode, Content imageContentNode) throws PathNotFoundException,
282             RepositoryException, IOException {
283 
284         // get the original image, as a buffered image
285         InputStream oriImgStr = parentContentNode.getNodeData(this.parentNodeDataName).getStream();
286         BufferedImage oriImgBuff = ImageIO.read(oriImgStr);
287         oriImgStr.close();
288         // create the new image file
289         File newImgFile = this.scaleImage(oriImgBuff);
290 
291         NodeDataUtil.getOrCreate(imageContentNode, "maxHeight").setValue(maxHeight);
292         NodeDataUtil.getOrCreate(imageContentNode, "maxWidth").setValue(maxWidth);
293 
294         createImageNode(newImgFile, imageContentNode);
295 
296         newImgFile.delete();
297     }
298 
299     /**
300      * Create an image file that is a scaled version of the original image.
301      *
302      * @param oriImgBuff the original image file
303      * @return the new image file
304      */
305     private File scaleImage(BufferedImage oriImgBuff) throws IOException {
306         // get the dimesnions of the original image
307         int oriWidth = oriImgBuff.getWidth();
308         int oriHeight = oriImgBuff.getHeight();
309         // get scale factor for the new image
310         double scaleFactor = this.scaleFactor(oriWidth, oriHeight);
311         // get the width and height of the new image
312         int newWidth = new Double(oriWidth * scaleFactor).intValue();
313         int newHeight = new Double(oriHeight * scaleFactor).intValue();
314         // create the thumbnail as a buffered image
315         Image newImg = oriImgBuff.getScaledInstance(newWidth, newHeight, Image.SCALE_AREA_AVERAGING);
316         BufferedImage newImgBuff = new BufferedImage(
317                 newImg.getWidth(null),
318                 newImg.getHeight(null),
319                 BufferedImage.TYPE_INT_RGB);
320         Graphics2D g = newImgBuff.createGraphics();
321         g.drawImage(newImg, 0, 0, null);
322         g.dispose();
323         // create the new image file in the temporary dir
324         File newImgFile = File.createTempFile(TEMP_IMAGE_NAME, PROPERTIES_EXTENSION_VALUE);
325 
326         ImageIO.write(newImgBuff, PROPERTIES_EXTENSION_VALUE, newImgFile);
327         // return the file
328         return newImgFile;
329     }
330 
331     /**
332      * Calculate the scale factor for the image.
333      *
334      * @param originalWidth the image width
335      * @param originalHeight the image height
336      * @return the scale factor
337      */
338     private double scaleFactor(int originalWidth, int originalHeight) {
339         double scaleFactor;
340 
341         int scaleWidth = this.maxWidth;
342         int scaleHeight = this.maxHeight;
343 
344         if (!this.allowOversize) {
345             scaleWidth = Math.min(this.maxWidth, originalWidth);
346             scaleHeight = Math.min(this.maxHeight, originalHeight);
347         }
348 
349         if (scaleWidth <= 0 && scaleHeight <= 0) {
350             // may a copy at the same size
351             scaleFactor = 1;
352         } else if (scaleWidth <= 0) {
353             // use height
354             scaleFactor = (double) scaleHeight / (double) originalHeight;
355         } else if (scaleHeight <= 0) {
356             // use width
357             scaleFactor = (double) scaleWidth / (double) originalWidth;
358         } else {
359             // create two scale factors, and see which is smaller
360             double scaleFactorWidth = (double) scaleWidth / (double) originalWidth;
361             double scaleFactorHeight = (double) scaleHeight / (double) originalHeight;
362             scaleFactor = Math.min(scaleFactorWidth, scaleFactorHeight);
363         }
364         return scaleFactor;
365     }
366 
367     @Override
368     protected String getFilename() {
369         return this.parentNodeDataName;
370     }
371 }