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