View Javadoc

1   /**
2    * This file Copyright (c) 2012 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.ItemType;
38  import info.magnolia.cms.util.ContentUtil;
39  
40  import java.awt.FontFormatException;
41  import java.awt.Graphics2D;
42  import java.awt.Image;
43  import java.awt.image.BufferedImage;
44  import java.io.File;
45  import java.io.FileNotFoundException;
46  import java.io.IOException;
47  import java.util.StringTokenizer;
48  
49  import javax.imageio.ImageIO;
50  import javax.jcr.AccessDeniedException;
51  import javax.jcr.PathNotFoundException;
52  import javax.jcr.RepositoryException;
53  import javax.servlet.http.HttpServletRequest;
54  import javax.servlet.jsp.JspException;
55  import javax.servlet.jsp.JspWriter;
56  import javax.servlet.jsp.PageContext;
57  
58  import org.apache.commons.lang.SystemUtils;
59  import org.slf4j.Logger;
60  import org.slf4j.LoggerFactory;
61  
62  import org.tldgen.annotations.BodyContent;
63  import org.tldgen.annotations.Tag;
64  
65  /**
66   * Tag that converts text into PNG images, and outputs a div element containing a set of img elements. The font face,
67   * text color, text size and background color can be set via the tag attributes. <br />
68   * <br />
69   * The images are saved under the node specified by the attribute parentContentNodeName. Under this parent node, a new
70   * node is created, with the name specified by the attribute imageContentNodeName. Under this node, each image will have
71   * it own node. The names of the image node are based on the text that they contain. (Special characters such as &, /,
72   * chinese characters, etc. are replaces by codes to ensure that these names are legal.) <br />
73   * <br />
74   * If the images for the specified text do not exist in the repository under the specified parent node, then the this
75   * tag will create the images and save them. If the images for the text already exist, then they will not be recreated.
76   * <br />
77   * <br />
78   * The text to be converted into images can be split in three ways. If the attribute textSplit is null or is set to
79   * TEXT_SPLIT_NONE, then a single image will be created of the text on one line. If textSplit is set to
80   * TEXT_SPLIT_WORDS, then the text is plit into words (i.e. wherever there is a space). Finally, if textSplit is set to
81   * TEXT_SPLIT_CHARACTERS, then a seperate image is created for each letter. <br />
82   * <br />
83   * The tag outputs a div that contains one or more img's. The CSS class applied to the div is specified by the divCSS
84   * attribute. The CSS applied to the images depends on how the text was split. For text that was not split, the CSS
85   * applied is set to CSS_TEXT_IMAGE, for words it is CSS_WORD_IMAGE, and for characters it is CSS_CHARACTER_IMAGE. Any
86   * spacing that is required between images will need to be set in your css stylesheet. <br />
87   * <br />
88   * The textFontFace attribute may either be a font name of a font installed on the server, or it may be a path to a TTF
89   * file. The class to generate PNG images from TrueType font strings is originally by Philip McCarthy -
90   * http://chimpen.com (http://chimpen.com/things/archives/001139.php). I have made a couple of small changes. <br />
91   * <br />
92   *
93   * @jsp.tag name="txt2img" body-content="empty"
94   *
95   * @author Patrick Janssen
96   * @author Fabrizio Giustina
97   * @version 1.0
98   */
99  @Tag(name="txt2img", bodyContent=BodyContent.EMPTY)
100 
101 public class TextToImageTag extends BaseImageTag {
102 
103     /**
104      * The image that is created can first be created at a larger size, and then scaled down. This overcomes kerning
105      * problems on the Windows platform, which results in very irregular spacing between characters. If you are not
106      * using Windows, this can be set to 1.
107      */
108     private static final double SCALE_FACTOR = SystemUtils.IS_OS_WINDOWS ? 4 : 1;
109 
110     /**
111      * The CSS class applied to images of individual characters.
112      */
113     private static final String CSS_CHARACTER_IMAGE = "character-image";
114 
115     /**
116      * The CSS class appled to images of words.
117      */
118     private static final String CSS_WORD_IMAGE = "word-image";
119 
120     /**
121      * The CSS class applied to images of whole sentances, or other text.
122      */
123     private static final String CSS_TEXT_IMAGE = "text-image";
124 
125     /**
126      * The text will not be split.
127      */
128     private static final String TEXT_SPLIT_NONE = "none";
129 
130     /**
131      * The text will be split into words.
132      */
133     private static final String TEXT_SPLIT_WORDS = "words";
134 
135     /**
136      * The text will be split into characters.
137      */
138     private static final String TEXT_SPLIT_CHARACTERS = "characters";
139 
140     private static final Logger log = LoggerFactory.getLogger(BaseImageTag.class);
141 
142     private String text;
143     private String textFontFace;
144     private int textFontSize;
145     private String textFontColor;
146     private String textBackColor;
147     private String textSplit;
148     private String divCSS;
149 
150     /**
151      * The text to be converted.
152      * @jsp.attribute required="true" rtexprvalue="true"
153      */
154     public void setText(String text) {
155         this.text = text;
156     }
157 
158     /**
159      * The name of the new contentNode to create, under which all image nodes will be saved.
160      * @jsp.attribute required="true" rtexprvalue="true"
161      */
162     @Override
163     public void setImageContentNodeName(String imageContentNodeName) {
164         this.imageContentNodeName = imageContentNodeName;
165     }
166 
167     /**
168      * The name of the parent of the new contentNode.
169      * @jsp.attribute required="false" rtexprvalue="true"
170      */
171     @Override
172     public void setParentContentNodeName(String parentContentNodeName) {
173         this.parentContentNodeName = parentContentNodeName;
174     }
175 
176     /**
177      * The font face of the text, e.g. 'Helvetica'. Default is 'Helvetica'.
178      * @jsp.attribute required="false" rtexprvalue="true"
179      */
180     public void setTextFontFace(String textFontFace) {
181         this.textFontFace = textFontFace;
182     }
183 
184     /**
185      * The size of the text, in points, e.g. 12. Default is '12'.
186      * @jsp.attribute required="false" rtexprvalue="true" type="int"
187      */
188     public void setTextFontSize(int textFontSize) {
189         this.textFontSize = textFontSize;
190     }
191 
192     /**
193      * The color of the text in hexadecimal format, e.g. 'ff0044'. Default is '000000' (black).
194      * @jsp.attribute required="false" rtexprvalue="true"
195      */
196     public void setTextFontColor(String textFontColor) {
197         this.textFontColor = textFontColor;
198     }
199 
200     /**
201      * The color of the background in hexadecimal format, e.g. 'ff0044'. Default is 'ffffff' (white).
202      * @jsp.attribute required="false" rtexprvalue="true"
203      */
204     public void setTextBackColor(String textBackColor) {
205         this.textBackColor = textBackColor;
206     }
207 
208     /**
209      * The method used to split the text into sub-strings: 'none', 'words', or 'characters'. Default is 'none'.
210      * @jsp.attribute required="false" rtexprvalue="true"
211      */
212     public void setTextSplit(String textSplit) {
213         this.textSplit = textSplit;
214     }
215 
216     /**
217      * The CSS class that will be applied to the div that contains these text images.
218      * Defaults to "text-box".
219      * @jsp.attribute required="false" rtexprvalue="true"
220      */
221     public void setDivCSS(String divCSS) {
222         this.divCSS = divCSS;
223     }
224 
225     /**
226      * @see info.magnolia.cms.taglibs.util.BaseImageTag#getFilename()
227      */
228     @Override
229     protected String getFilename() {
230         return "textImage";
231     }
232 
233     /**
234      * Initialize settings.
235      */
236     public void setUp() {
237 
238         // check that all the necessary attributes are set
239         if (this.text == null) {
240             this.text = "Test Test Test";
241         }
242         if (this.textFontFace == null) {
243             this.textFontFace = SystemUtils.IS_OS_WINDOWS ? "Arial" : "Helvetica";
244         }
245         if (this.textFontSize == 0) {
246             this.textFontSize = 12;
247         }
248         if (this.textFontColor == null) {
249             this.textFontColor = "000000";
250         }
251         if (this.textBackColor == null) {
252             this.textBackColor = "ffffff";
253         }
254         if (this.textSplit == null) {
255             this.textSplit = TEXT_SPLIT_NONE;
256         }
257         else if (!((this.textSplit.equals(TEXT_SPLIT_WORDS)) || (this.textSplit.equals(TEXT_SPLIT_CHARACTERS)))) {
258             this.textSplit = TEXT_SPLIT_NONE;
259         }
260         if (this.divCSS == null) {
261             this.divCSS = "text-box";
262         }
263     }
264 
265     @Override
266     public void doTag() throws JspException {
267 
268         this.setUp();
269 
270         try {
271             Content imageContentNode = ContentUtil.asContent(getImageContentNode());
272 
273             String[] subStrings = this.getTextSubStrings(this.text);
274             String[] imageURIs = this.getImageURIs(
275                 subStrings,
276                 (HttpServletRequest) ((PageContext) this.getJspContext()).getRequest(),
277                 imageContentNode);
278             this.drawTextImages(imageURIs, subStrings);
279         }
280         catch (PathNotFoundException e) {
281             log.error("PathNotFoundException occured during text-to-image conversion: " + e.getMessage(), e);
282         }
283         catch (AccessDeniedException e) {
284             log.error("AccessDeniedException occured during text-to-image conversion: " + e.getMessage(), e);
285         }
286         catch (RepositoryException e) {
287             log.error("RepositoryException occured during text-to-image conversion: " + e.getMessage(), e);
288         }
289         catch (FileNotFoundException e) {
290             log.error("FileNotFoundException occured during text-to-image conversion: " + e.getMessage(), e);
291         }
292         catch (IOException e) {
293             log.error("IOException occured during text-to-image conversion: " + e.getMessage(), e);
294         }
295         catch (FontFormatException e) {
296             log.error("FontFormatException occured during text-to-image conversion: " + e.getMessage(), e);
297         }
298         this.cleanUp();
299     }
300 
301     /**
302      * Set objects to null.
303      */
304     public void cleanUp() {
305         this.parentContentNodeName = null;
306         this.imageContentNodeName = null;
307         this.text = null;
308         this.textFontFace = null;
309         this.textFontSize = 0;
310         this.textFontColor = null;
311         this.textBackColor = null;
312         this.textSplit = null;
313         this.divCSS = null;
314     }
315 
316     /**
317      * Draws a div box that contains the text images.
318      * @param imageURLs an array of urls
319      * @param subStrings an array of strings
320      * @throws IOException jspwriter exception
321      */
322     private void drawTextImages(String[] imageURIs, String[] subStrings) throws IOException {
323         JspWriter out = this.getJspContext().getOut();
324 
325         if (this.divCSS != null) {
326             out.print("<div class=\"");
327             out.print(this.divCSS);
328             out.print("\">");
329         }
330 
331         for (int i = 0; i < imageURIs.length; i++) {
332             out.print("<img class=\"");
333             if (this.textSplit.equals(TEXT_SPLIT_CHARACTERS)) {
334                 out.print(CSS_CHARACTER_IMAGE);
335             }
336             else if (this.textSplit.equals(TEXT_SPLIT_WORDS)) {
337                 out.print(CSS_WORD_IMAGE);
338             }
339             else {
340                 out.print(CSS_TEXT_IMAGE);
341             }
342             out.print("\" src=\"");
343             out.print(imageURIs[i]);
344             out.print("\" alt=\"");
345             out.print(subStrings[i]);
346             out.print("\" />");
347         }
348 
349         if (this.divCSS != null) {
350             out.print("</div>");
351         }
352     }
353 
354     /**
355      * Splits a string into words or characters, depending on the textSplit attribute. For words, spaces at either end
356      * are removed.
357      * @param The string to split
358      * @return An array of words
359      */
360     private String[] getTextSubStrings(String text) {
361         String[] subStrings = null;
362         if (this.textSplit.equals(TEXT_SPLIT_CHARACTERS)) {
363             subStrings = new String[text.length()];
364             for (int i = 0; i < text.length(); i++) {
365                 subStrings[i] = text.substring(i, i + 1);
366             }
367         }
368         else if (this.textSplit.equals(TEXT_SPLIT_WORDS)) {
369             StringTokenizer st = new StringTokenizer(text, " "); // Split sentence into words
370             subStrings = new String[st.countTokens()];
371             for (int i = 0; st.hasMoreTokens(); i++) {
372                 subStrings[i] = st.nextToken().trim();
373             }
374         }
375         else {
376             subStrings = new String[]{text};
377         }
378         return subStrings;
379     }
380 
381     /**
382      * Get an array of image URIs, one URI for each text string.
383      * @param The array of text strings.
384      * @return An array of URIs pointing to the images.
385      */
386     private String[] getImageURIs(String[] subStrings, HttpServletRequest req, Content imageContentNode)
387         throws PathNotFoundException, AccessDeniedException, RepositoryException, FileNotFoundException, IOException,
388         FontFormatException {
389 
390         String[] imageURIs = new String[subStrings.length];
391         for (int i = 0; i < subStrings.length; i++) {
392             // Create a unique image node name
393             String tmpImgNodeName = subStrings[i]
394                 + this.textBackColor
395                 + this.textFontColor
396                 + this.textFontFace
397                 + this.textFontSize;
398             String imageNodeName = this.convertToSimpleString(tmpImgNodeName);
399             // If the image node with this name does not exist, then create it.
400             if (!imageContentNode.hasContent(imageNodeName)) {
401                 File image = createImage(subStrings[i]);
402 
403                 // Create the node that will contain the image
404                 Content imageNode = imageContentNode.createContent(imageNodeName, ItemType.CONTENTNODE);
405 
406                 this.createImageNode(image, imageNode);
407             }
408             // Save the URI for this image in the array
409             String contextPath = req.getContextPath();
410             String handle = imageContentNode.getHandle();
411             String imageURI = contextPath
412                 + handle
413                 + "/"
414                 + imageNodeName
415                 + "/"
416                 + getFilename()
417                 + "."
418                 + PROPERTIES_EXTENSION_VALUE;
419             imageURIs[i] = imageURI;
420         }
421         return imageURIs;
422     }
423 
424     /**
425      * Creates an image from a word. The file is saved in the location specified by TEMP_IMAGE_PATH.
426      * @param subString The text.
427      * @return An input stream.
428      */
429     private File createImage(String subString) throws FileNotFoundException, IOException, FontFormatException {
430 
431         // Create file
432         File imageFile = File.createTempFile(getClass().getName(), "png");
433         imageFile.createNewFile();
434 
435         // create the image
436         // due to kerning problems, the image is being created 4 times to big
437         // then being scaled down to the right size
438         Text2PngFactory tpf = new Text2PngFactory();
439         tpf.setFontFace(this.textFontFace);
440         tpf.setFontSize((int) (this.textFontSize * SCALE_FACTOR));
441         int[] textRGB = this.convertHexToRGB(this.textFontColor);
442         int[] backRGB = this.convertHexToRGB(this.textBackColor);
443         tpf.setTextRGB(textRGB[0], textRGB[1], textRGB[2]);
444         tpf.setBackgroundRGB(backRGB[0], backRGB[1], backRGB[2]);
445         tpf.setText(subString);
446 
447         BufferedImage bigImgBuff = (BufferedImage) tpf.createImage();
448         if (SCALE_FACTOR != 1) {
449             BufferedImage smallImgBuff = this.scaleImage(bigImgBuff, (1.0 / SCALE_FACTOR));
450             ImageIO.write(smallImgBuff, "png", imageFile);
451             smallImgBuff = null;
452         }
453         else {
454             ImageIO.write(bigImgBuff, "png", imageFile);
455         }
456         bigImgBuff = null;
457         return imageFile;
458     }
459 
460     /**
461      * Create an image file that is a scaled version of the original image.
462      * @param the original BufferedImage
463      * @param the scale factor
464      * @return the new BufferedImage
465      */
466     private BufferedImage scaleImage(BufferedImage oriImgBuff, double scaleFactor) {
467 
468         // get the dimesnions of the original image
469         int oriWidth = oriImgBuff.getWidth();
470         int oriHeight = oriImgBuff.getHeight();
471         // get the width and height of the new image
472         int newWidth = new Double(oriWidth * scaleFactor).intValue();
473         int newHeight = new Double(oriHeight * scaleFactor).intValue();
474         // create the thumbnail as a buffered image
475         Image newImg = oriImgBuff.getScaledInstance(newWidth, newHeight, Image.SCALE_AREA_AVERAGING);
476         BufferedImage newImgBuff = new BufferedImage(
477             newImg.getWidth(null),
478             newImg.getHeight(null),
479             BufferedImage.TYPE_INT_RGB);
480         Graphics2D g = newImgBuff.createGraphics();
481         g.drawImage(newImg, 0, 0, null);
482         g.dispose();
483         // return the newImgBuff
484         return newImgBuff;
485     }
486 
487 }