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.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  import org.tldgen.annotations.BodyContent;
62  import org.tldgen.annotations.Tag;
63  
64  /**
65   * Tag that converts text into PNG images, and outputs a div element containing a set of img elements. The font face,
66   * text color, text size and background color can be set via the tag attributes. <br />
67   * <br />
68   * The images are saved under the node specified by the attribute parentContentNodeName. Under this parent node, a new
69   * node is created, with the name specified by the attribute imageContentNodeName. Under this node, each image will have
70   * it own node. The names of the image node are based on the text that they contain. (Special characters such as &amp;,
71   * /, chinese characters, etc. are replaces by codes to ensure that these names are legal.) <br />
72   * <br />
73   * If the images for the specified text do not exist in the repository under the specified parent node, then the this
74   * tag will create the images and save them. If the images for the text already exist, then they will not be recreated.
75   * <br />
76   * <br />
77   * The text to be converted into images can be split in three ways. If the attribute textSplit is null or is set to
78   * TEXT_SPLIT_NONE, then a single image will be created of the text on one line. If textSplit is set to
79   * TEXT_SPLIT_WORDS, then the text is plit into words (i.e. wherever there is a space). Finally, if textSplit is set to
80   * TEXT_SPLIT_CHARACTERS, then a seperate image is created for each letter. <br />
81   * <br />
82   * The tag outputs a div that contains one or more img's. The CSS class applied to the div is specified by the divCSS
83   * attribute. The CSS applied to the images depends on how the text was split. For text that was not split, the CSS
84   * applied is set to CSS_TEXT_IMAGE, for words it is CSS_WORD_IMAGE, and for characters it is CSS_CHARACTER_IMAGE. Any
85   * spacing that is required between images will need to be set in your css stylesheet. <br />
86   * <br />
87   * 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
88   * file. The class to generate PNG images from TrueType font strings is originally by Philip McCarthy -
89   * http://chimpen.com (http://chimpen.com/things/archives/001139.php). I have made a couple of small changes. <br />
90   * <br />
91   *
92   * @jsp.tag name="txt2img" body-content="empty"
93   */
94  @Tag(name = "txt2img", bodyContent = BodyContent.EMPTY)
95  
96  public class TextToImageTag extends BaseImageTag {
97  
98      /**
99       * The image that is created can first be created at a larger size, and then scaled down. This overcomes kerning
100      * problems on the Windows platform, which results in very irregular spacing between characters. If you are not
101      * using Windows, this can be set to 1.
102      */
103     private static final double SCALE_FACTOR = SystemUtils.IS_OS_WINDOWS ? 4 : 1;
104 
105     /**
106      * The CSS class applied to images of individual characters.
107      */
108     private static final String CSS_CHARACTER_IMAGE = "character-image";
109 
110     /**
111      * The CSS class appled to images of words.
112      */
113     private static final String CSS_WORD_IMAGE = "word-image";
114 
115     /**
116      * The CSS class applied to images of whole sentances, or other text.
117      */
118     private static final String CSS_TEXT_IMAGE = "text-image";
119 
120     /**
121      * The text will not be split.
122      */
123     private static final String TEXT_SPLIT_NONE = "none";
124 
125     /**
126      * The text will be split into words.
127      */
128     private static final String TEXT_SPLIT_WORDS = "words";
129 
130     /**
131      * The text will be split into characters.
132      */
133     private static final String TEXT_SPLIT_CHARACTERS = "characters";
134 
135     private static final Logger log = LoggerFactory.getLogger(BaseImageTag.class);
136 
137     private String text;
138     private String textFontFace;
139     private int textFontSize;
140     private String textFontColor;
141     private String textBackColor;
142     private String textSplit;
143     private String divCSS;
144 
145     /**
146      * The text to be converted.
147      *
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      *
157      * @jsp.attribute required="true" rtexprvalue="true"
158      */
159     @Override
160     public void setImageContentNodeName(String imageContentNodeName) {
161         this.imageContentNodeName = imageContentNodeName;
162     }
163 
164     /**
165      * The name of the parent of the new contentNode.
166      *
167      * @jsp.attribute required="false" rtexprvalue="true"
168      */
169     @Override
170     public void setParentContentNodeName(String parentContentNodeName) {
171         this.parentContentNodeName = parentContentNodeName;
172     }
173 
174     /**
175      * The font face of the text, e.g. 'Helvetica'. Default is 'Helvetica'.
176      *
177      * @jsp.attribute required="false" rtexprvalue="true"
178      */
179     public void setTextFontFace(String textFontFace) {
180         this.textFontFace = textFontFace;
181     }
182 
183     /**
184      * The size of the text, in points, e.g. 12. Default is '12'.
185      *
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      *
195      * @jsp.attribute required="false" rtexprvalue="true"
196      */
197     public void setTextFontColor(String textFontColor) {
198         this.textFontColor = textFontColor;
199     }
200 
201     /**
202      * The color of the background in hexadecimal format, e.g. 'ff0044'. Default is 'ffffff' (white).
203      *
204      * @jsp.attribute required="false" rtexprvalue="true"
205      */
206     public void setTextBackColor(String textBackColor) {
207         this.textBackColor = textBackColor;
208     }
209 
210     /**
211      * The method used to split the text into sub-strings: 'none', 'words', or 'characters'. Default is 'none'.
212      *
213      * @jsp.attribute required="false" rtexprvalue="true"
214      */
215     public void setTextSplit(String textSplit) {
216         this.textSplit = textSplit;
217     }
218 
219     /**
220      * The CSS class that will be applied to the div that contains these text images.
221      * Defaults to "text-box".
222      *
223      * @jsp.attribute required="false" rtexprvalue="true"
224      */
225     public void setDivCSS(String divCSS) {
226         this.divCSS = divCSS;
227     }
228 
229     /**
230      * @see info.magnolia.cms.taglibs.util.BaseImageTag#getFilename()
231      */
232     @Override
233     protected String getFilename() {
234         return "textImage";
235     }
236 
237     /**
238      * Initialize settings.
239      */
240     public void setUp() {
241 
242         // check that all the necessary attributes are set
243         if (this.text == null) {
244             this.text = "Test Test Test";
245         }
246         if (this.textFontFace == null) {
247             this.textFontFace = SystemUtils.IS_OS_WINDOWS ? "Arial" : "Helvetica";
248         }
249         if (this.textFontSize == 0) {
250             this.textFontSize = 12;
251         }
252         if (this.textFontColor == null) {
253             this.textFontColor = "000000";
254         }
255         if (this.textBackColor == null) {
256             this.textBackColor = "ffffff";
257         }
258         if (this.textSplit == null) {
259             this.textSplit = TEXT_SPLIT_NONE;
260         } else if (!((this.textSplit.equals(TEXT_SPLIT_WORDS)) || (this.textSplit.equals(TEXT_SPLIT_CHARACTERS)))) {
261             this.textSplit = TEXT_SPLIT_NONE;
262         }
263         if (this.divCSS == null) {
264             this.divCSS = "text-box";
265         }
266     }
267 
268     @Override
269     public void doTag() throws JspException {
270 
271         this.setUp();
272 
273         try {
274             Content imageContentNode = ContentUtil.asContent(getImageContentNode());
275 
276             String[] subStrings = this.getTextSubStrings(this.text);
277             String[] imageURIs = this.getImageURIs(
278                     subStrings,
279                     (HttpServletRequest) ((PageContext) this.getJspContext()).getRequest(),
280                     imageContentNode);
281             this.drawTextImages(imageURIs, subStrings);
282         } catch (PathNotFoundException e) {
283             log.error("PathNotFoundException occured during text-to-image conversion: {}", e.getMessage(), e);
284         } catch (AccessDeniedException e) {
285             log.error("AccessDeniedException occured during text-to-image conversion: {}", e.getMessage(), e);
286         } catch (RepositoryException e) {
287             log.error("RepositoryException occured during text-to-image conversion: {}", e.getMessage(), e);
288         } catch (FileNotFoundException e) {
289             log.error("FileNotFoundException occured during text-to-image conversion: {}", e.getMessage(), e);
290         } catch (IOException e) {
291             log.error("IOException occured during text-to-image conversion: {}", e.getMessage(), e);
292         } catch (FontFormatException e) {
293             log.error("FontFormatException occured during text-to-image conversion: {}", e.getMessage(), e);
294         }
295         this.cleanUp();
296     }
297 
298     /**
299      * Set objects to null.
300      */
301     public void cleanUp() {
302         this.parentContentNodeName = null;
303         this.imageContentNodeName = null;
304         this.text = null;
305         this.textFontFace = null;
306         this.textFontSize = 0;
307         this.textFontColor = null;
308         this.textBackColor = null;
309         this.textSplit = null;
310         this.divCSS = null;
311     }
312 
313     /**
314      * Draws a div box that contains the text images.
315      *
316      * @param imageURLs an array of urls
317      * @param subStrings an array of strings
318      * @throws IOException jspwriter exception
319      */
320     private void drawTextImages(String[] imageURIs, String[] subStrings) throws IOException {
321         JspWriter out = this.getJspContext().getOut();
322 
323         if (this.divCSS != null) {
324             out.print("<div class=\"");
325             out.print(this.divCSS);
326             out.print("\">");
327         }
328 
329         for (int i = 0; i < imageURIs.length; i++) {
330             out.print("<img class=\"");
331             if (this.textSplit.equals(TEXT_SPLIT_CHARACTERS)) {
332                 out.print(CSS_CHARACTER_IMAGE);
333             } else if (this.textSplit.equals(TEXT_SPLIT_WORDS)) {
334                 out.print(CSS_WORD_IMAGE);
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      *
354      * @param The string to split
355      * @return An array of words
356      */
357     private String[] getTextSubStrings(String text) {
358         String[] subStrings = null;
359         if (this.textSplit.equals(TEXT_SPLIT_CHARACTERS)) {
360             subStrings = new String[text.length()];
361             for (int i = 0; i < text.length(); i++) {
362                 subStrings[i] = text.substring(i, i + 1);
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         } else {
371             subStrings = new String[]{text};
372         }
373         return subStrings;
374     }
375 
376     /**
377      * Get an array of image URIs, one URI for each text string.
378      *
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      *
423      * @param subString The text.
424      * @return An input stream.
425      */
426     private File createImage(String subString) throws FileNotFoundException, IOException, FontFormatException {
427 
428         // Create file
429         File imageFile = File.createTempFile(getClass().getName(), "png");
430         imageFile.createNewFile();
431 
432         // create the image
433         // due to kerning problems, the image is being created 4 times to big
434         // then being scaled down to the right size
435         Text2PngFactory tpf = new Text2PngFactory();
436         tpf.setFontFace(this.textFontFace);
437         tpf.setFontSize((int) (this.textFontSize * SCALE_FACTOR));
438         int[] textRGB = this.convertHexToRGB(this.textFontColor);
439         int[] backRGB = this.convertHexToRGB(this.textBackColor);
440         tpf.setTextRGB(textRGB[0], textRGB[1], textRGB[2]);
441         tpf.setBackgroundRGB(backRGB[0], backRGB[1], backRGB[2]);
442         tpf.setText(subString);
443 
444         BufferedImage bigImgBuff = (BufferedImage) tpf.createImage();
445         if (SCALE_FACTOR != 1) {
446             BufferedImage smallImgBuff = this.scaleImage(bigImgBuff, (1.0 / SCALE_FACTOR));
447             ImageIO.write(smallImgBuff, "png", imageFile);
448             smallImgBuff = null;
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      *
459      * @param the original BufferedImage
460      * @param the scale factor
461      * @return the new BufferedImage
462      */
463     private BufferedImage scaleImage(BufferedImage oriImgBuff, double scaleFactor) {
464 
465         // get the dimesnions of the original image
466         int oriWidth = oriImgBuff.getWidth();
467         int oriHeight = oriImgBuff.getHeight();
468         // get the width and height of the new image
469         int newWidth = new Double(oriWidth * scaleFactor).intValue();
470         int newHeight = new Double(oriHeight * scaleFactor).intValue();
471         // create the thumbnail as a buffered image
472         Image newImg = oriImgBuff.getScaledInstance(newWidth, newHeight, Image.SCALE_AREA_AVERAGING);
473         BufferedImage newImgBuff = new BufferedImage(
474                 newImg.getWidth(null),
475                 newImg.getHeight(null),
476                 BufferedImage.TYPE_INT_RGB);
477         Graphics2D g = newImgBuff.createGraphics();
478         g.drawImage(newImg, 0, 0, null);
479         g.dispose();
480         // return the newImgBuff
481         return newImgBuff;
482     }
483 
484 }