001    package railo.runtime.tag;
002    
003    import java.awt.Color;
004    import java.io.IOException;
005    
006    import javax.servlet.jsp.JspException;
007    
008    import railo.commons.color.ColorCaster;
009    import railo.commons.io.res.Resource;
010    import railo.commons.io.res.util.ResourceUtil;
011    import railo.commons.lang.StringUtil;
012    import railo.runtime.exp.ApplicationException;
013    import railo.runtime.exp.ExpressionException;
014    import railo.runtime.exp.PageException;
015    import railo.runtime.ext.tag.TagImpl;
016    import railo.runtime.functions.other.CreateUUID;
017    import railo.runtime.functions.system.ContractPath;
018    import railo.runtime.img.ImageUtil;
019    import railo.runtime.img.MarpleCaptcha;
020    import railo.runtime.op.Caster;
021    import railo.runtime.type.List;
022    import railo.runtime.type.Struct;
023    import railo.runtime.type.util.ArrayUtil;
024    
025    // GetWriteableImageFormats
026    // GetReadableImageFormats
027    
028    
029    /**
030    * Lets you resize and add labels to GIF and JPEG format images.
031    *
032    *
033    *
034    **/
035    public final class Image extends TagImpl {
036            
037            private static int ACTION_BORDER=0;
038            private static int ACTION_CAPTCHA=1;
039            private static int ACTION_CONVERT=2;
040            private static int ACTION_INFO=3;
041            private static int ACTION_READ=4;
042            private static int ACTION_RESIZE=5;
043            private static int ACTION_ROTATE=6;
044            private static int ACTION_WRITE=7;
045            private static int ACTION_WRITE_TO_BROWSER=8;
046    
047    
048            private int action=ACTION_READ;
049            private String strAction="read";
050            private int angle=-1;
051            private Color color=Color.BLACK;
052            private Resource destination;
053            private int difficulty=MarpleCaptcha.DIFFICULTY_LOW;
054            private String[] fonts=new String[]{"arial"};
055            private int fontsize=24;
056            private String format="png";
057            private String height;
058            private String width;
059            private boolean isbase64;
060            private String name;
061            private boolean overwrite;
062            private float quality=.75F;
063            private Object oSource;
064            private railo.runtime.img.Image source;
065            private String structName;
066            private String text;
067            private int thickness=1;
068            private String passthrough;
069            private boolean base64;
070            
071    
072    
073    
074            /**
075             *
076             * @see railo.runtime.ext.tag.TagImpl#release()
077             */
078            public void release() {
079                    super.release();
080                    action=ACTION_READ;
081                    strAction="read";
082                    angle=-1;
083                    color=Color.BLACK;
084                    destination=null;
085                    difficulty=MarpleCaptcha.DIFFICULTY_LOW;
086                    fonts=new String[]{"arial"};
087                    fontsize=24;
088                    format="png";
089                    height=null;
090                    width=null;
091                    isbase64=false;
092                    name=null;
093                    overwrite=false;
094                    quality=0.75F;
095                    source=null;
096                    oSource=null;
097                    structName=null;
098                    text=null;
099                    thickness=1;
100                    passthrough=null;
101                    base64=false;
102            }
103    
104    
105            /**
106             * @param action the action to set
107             * @throws ApplicationException 
108             */
109            public void setAction(String strAction) throws ApplicationException {
110                    this.strAction = strAction;
111                    strAction=strAction.trim().toLowerCase();
112                    if(StringUtil.isEmpty(strAction))action=ACTION_READ;
113                    else if("border".equals(strAction))action=ACTION_BORDER;
114                    else if("captcha".equals(strAction))action=ACTION_CAPTCHA;
115                    else if("convert".equals(strAction))action=ACTION_CONVERT;
116                    else if("info".equals(strAction))action=ACTION_INFO;
117                    else if("read".equals(strAction))action=ACTION_READ;
118                    else if("resize".equals(strAction))action=ACTION_RESIZE;
119                    else if("rotate".equals(strAction))action=ACTION_ROTATE;
120                    else if("write".equals(strAction))action=ACTION_WRITE;
121                    else if("writetobrowser".equals(strAction))action=ACTION_WRITE_TO_BROWSER;
122                    else if("write-to-browser".equals(strAction))action=ACTION_WRITE_TO_BROWSER;
123                    else if("write_to_browser".equals(strAction))action=ACTION_WRITE_TO_BROWSER;
124                    else throw new ApplicationException("invalid action ["+this.strAction+"], " +
125                    "valid actions are [border,captcha,convert,info,read,resize,rotate,write,writeToBrowser]");
126            }
127    
128    
129            /**
130             * @param base64 the base64 to set
131             */
132            public void setBase64(boolean base64) {
133                    this.base64 = base64;
134            }
135    
136    
137            /**
138             * @param angle the angle to set
139             */
140            public void setAngle(double angle) {
141                    this.angle = (int) angle;
142            }
143    
144    
145            /**
146             * @param color the color to set
147             * @throws ExpressionException 
148             */
149            public void setColor(String strColor) throws ExpressionException {
150                    this.color = ColorCaster.toColor(strColor);
151            }
152    
153    
154            /**
155             * @param destination the destination to set
156             */
157            public void setDestination(String destination) {
158                    this.destination = ResourceUtil.toResourceNotExisting(pageContext, destination);
159            }
160    
161    
162            /**
163             * @param difficulty the difficulty to set
164             * @throws ApplicationException 
165             */
166            public void setDifficulty(String strDifficulty) throws ApplicationException {
167                    strDifficulty=strDifficulty.trim().toLowerCase();
168                    if(StringUtil.isEmpty(strDifficulty))   difficulty=MarpleCaptcha.DIFFICULTY_LOW;
169                    else if("low".equals(strDifficulty))    difficulty=MarpleCaptcha.DIFFICULTY_LOW;
170                    else if("medium".equals(strDifficulty)) difficulty=MarpleCaptcha.DIFFICULTY_MEDIUM;
171                    else if("high".equals(strDifficulty))   difficulty=MarpleCaptcha.DIFFICULTY_HIGH;
172                    else throw new ApplicationException("invalid difficulty level ["+strDifficulty+"], " +
173                    "valid difficulty level are [low,medium,high]");
174                    
175            }
176    
177    
178            /**
179             * @param fonts the fonts to set
180             * @throws PageException 
181             */
182            public void setFonts(String fontList) throws PageException {
183                    fonts=ArrayUtil.trim(List.toStringArray(List.listToArray(fontList, ',')));
184            }
185    
186            
187    
188            /**
189             * @param passthrough the passthrough to set
190             */
191            public void setPassthrough(String passthrough) {
192                    this.passthrough = passthrough;
193            }
194    
195            /**
196             * @param fontsize the fontsize to set
197             */
198            public void setFontsize(double fontsize) {
199                    this.fontsize = (int) fontsize;
200            }
201    
202    
203            /**
204             * @param format the format to set
205             * @throws ApplicationException 
206             */
207            public void setFormat(String format) throws ApplicationException {
208                    format=format.trim().toLowerCase();
209                    if("gif".equalsIgnoreCase(format))              this.format =  "gif";
210                    else if("jpg".equalsIgnoreCase(format)) this.format =  "jpg";
211                    else if("jpe".equalsIgnoreCase(format)) this.format =  "jpg";
212                    else if("jpeg".equalsIgnoreCase(format))this.format =  "jpg";
213                    else if("png".equalsIgnoreCase(format)) this.format =  "png";
214                    else if("tiff".equalsIgnoreCase(format))this.format =  "tiff";
215                    else if("bmp".equalsIgnoreCase(format)) this.format =  "bmp";
216                    else throw new ApplicationException("invalid format ["+format+"], " +
217                    "valid formats are [gif,jpg,png,tiff,bmp]");
218            }
219    
220    
221            /**
222             * @param height the height to set
223             */
224            public void setHeight(String height) {
225                    this.height = height;
226            }
227    
228    
229            /**
230             * @param width the width to set
231             */
232            public void setWidth(String width) {
233                    this.width = width;
234            }
235    
236    
237            /**
238             * @param isbase64 the isbase64 to set
239             */
240            public void setIsbase64(boolean isbase64) {
241                    this.isbase64 = isbase64;
242            }
243    
244    
245            /**
246             * @param name the name to set
247             */
248            public void setName(String name) {
249                    this.name = name;
250            }
251    
252    
253            /**
254             * @param overwrite the overwrite to set
255             */
256            public void setOverwrite(boolean overwrite) {
257                    this.overwrite = overwrite;
258            }
259    
260    
261            /**
262             * @param quality the quality to set
263             * @throws ApplicationException 
264             */
265            public void setQuality(double quality) throws ApplicationException {
266                    this.quality = (float) quality;
267                    if(quality<0 || quality>1)
268                            throw new ApplicationException("quality ("+Caster.toString(quality)+") has to be a value between 0 and 1");
269            }
270    
271    
272            /**
273             * @param source the source to set
274             * @throws PageException 
275             */
276            public void setSource(Object source) {
277                    this.oSource=source;
278                    // this.source=railo.runtime.img.Image.createImage(pageContext, source, false, false);
279            }
280    
281    
282            /**
283             * @param structName the structName to set
284             */
285            public void setStructname(String structName) {
286                    this.structName = structName;
287            }
288    
289    
290            /**
291             * @param structName the structName to set
292             */
293            public void setResult(String structName) {
294                    this.structName = structName;
295            }
296    
297    
298            /**
299             * @param text the text to set
300             */
301            public void setText(String text) {
302                    this.text = text;
303            }
304    
305    
306            /**
307             * @param thickness the thickness to set
308             */
309            public void setThickness(double thickness) {
310                    this.thickness = (int) thickness;
311            }
312    
313    
314            /**
315             *
316             * @see railo.runtime.ext.tag.TagImpl#doStartTag()
317             */
318            public int doStartTag() throws JspException {
319                    try {
320                            if(this.oSource!=null){
321                                    if(isbase64) this.source=new railo.runtime.img.Image(Caster.toString(oSource));
322                                    else this.source=railo.runtime.img.Image.createImage(pageContext, oSource, false, false,true);
323                            }
324                            
325                            if(action==ACTION_BORDER)               doActionBorder();
326                            else if(action==ACTION_CAPTCHA) doActionCaptcha();
327                            else if(action==ACTION_CONVERT) doActionConvert();
328                            else if(action==ACTION_INFO)    doActionInfo();
329                            else if(action==ACTION_READ)    doActionRead();
330                            else if(action==ACTION_RESIZE)  doActionResize();
331                            else if(action==ACTION_ROTATE)  doActionRotate();
332                            else if(action==ACTION_WRITE)   doActionWrite();
333                            else if(action==ACTION_WRITE_TO_BROWSER) 
334                                                                                            doActionWriteToBrowser();
335                    }
336                    catch(Throwable t) {
337                            throw Caster.toPageException(t);
338                    }
339                    return SKIP_BODY;
340            }
341    
342            // Add a border to an image 
343            private void doActionBorder() throws PageException, IOException {
344                    required("source", source);
345                    
346                    source.addBorder(thickness,color,railo.runtime.img.Image.BORDER_TYPE_CONSTANT);
347                    write();
348                        
349            }
350    
351    
352            // Create a CAPTCHA image
353            private void doActionCaptcha() throws PageException, IOException {
354                    required("height", height);
355                    required("width", width);
356                    required("text", text);
357                    
358                    String path=null;
359                    
360                    // create destination
361                    if(StringUtil.isEmpty(name))path=touchDestination();
362                    
363                    MarpleCaptcha c=new MarpleCaptcha();
364                    source=new railo.runtime.img.Image(c.generate(text, Caster.toIntValue(width),Caster.toIntValue(height), fonts, true, 
365                                    Color.BLACK,fontsize,
366                                    difficulty));
367                    
368                    // link destination
369                    if(StringUtil.isEmpty(name))writeLink(path);
370                    
371                    // write out
372                    write();
373            }
374            
375            private void writeLink(String path) throws IOException, PageException {
376                    String add="";
377                    if(passthrough!=null) {
378                add=" "+passthrough;
379            }
380                    
381                    if(base64) {
382                            String b64 = source.getBase64String(format);
383                            pageContext.write("<img src=\"data:image/"+ImageUtil.getMimeTypeFromFormat(format)+";base64,"+b64+"\" width=\""+source.getWidth()+"\" height=\""+source.getHeight()+"\""+add+" />");
384                            return;
385                    }
386                    pageContext.write("<img src=\""+path+"\" width=\""+source.getWidth()+"\" height=\""+source.getHeight()+"\""+add+" />");
387            
388            
389            }
390    
391    
392            private String touchDestination() throws IOException {
393                    if(destination==null) {
394                            String name=CreateUUID.call(pageContext)+"."+format;
395                            Resource folder = pageContext.getConfig().getTempDirectory().getRealResource("graph");
396                            if(!folder.exists())folder.createDirectory(true);
397                            destination = folder.getRealResource(name);
398                            cleanOld(folder);
399                            
400                            // create path
401                            String cp = pageContext.getHttpServletRequest().getContextPath();
402                            if(StringUtil.isEmpty(cp)) cp="";
403                            return cp+"/railo-context/graph.cfm?img="+name+"&type="+(List.last(ImageUtil.getMimeTypeFromFormat(format),'/').trim());
404                    }
405                    return ContractPath.call(pageContext, destination.getAbsolutePath());
406            }
407    
408    
409            private static void cleanOld(Resource folder) throws IOException {
410                    if(!folder.exists())folder.createDirectory(true);
411                    else if(folder.isDirectory() && ResourceUtil.getRealSize(folder)>(1024*1024)) {
412                            
413                            Resource[] children = folder.listResources();
414                            long maxAge=System.currentTimeMillis()-(1000*60);
415                            for(int i=0;i<children.length;i++) {
416                                    if(children[i].lastModified()<maxAge)
417                                            children[i].delete();
418                            }
419                    }
420            }
421    
422            // Convert an image file format
423            private void doActionConvert() throws PageException, IOException {
424                    required("source", source);
425                    required("destination", destination);
426                    
427                    source.convert(ImageUtil.getFormat(destination));
428                    write();
429            }
430    
431            // Retrieve information about an image
432            private void doActionInfo() throws PageException {
433                    required("source",source);
434                    required("structname",structName);
435                    
436                    pageContext.setVariable(structName, source.info());
437            }
438            
439            private Struct doActionInfo(railo.runtime.img.Image source) throws PageException {
440                    return source.info();
441            }
442    
443    
444            // Read an image into memory
445            private void doActionRead() throws PageException {
446                    required("source",source);
447                    required("name",name);
448                    
449                    pageContext.setVariable(name, source);
450            }
451    
452    
453            // Resize an image
454            private void doActionResize() throws PageException, IOException {
455                    required("source", source);
456                    
457                    Info i = new Info(source);
458                    /*int w = toDimension("width",width,i);
459                    int h = toDimension("height",height,i);
460                    if(w==-1 && h==-1)
461                            throw new ApplicationException("Missing attribute [width or height]. The action ["+strAction+"] requires the attribute [width or height].");
462                    */
463                    
464                    source.resize(width,height,"highestquality",1D);
465                    
466                    write();
467            }
468            
469            // Rotate an image
470            private void doActionRotate() throws PageException, IOException {
471                    required("source",source);
472                    required("angle",angle,-1);
473                    
474                    source.rotate(-1, -1, angle, railo.runtime.img.Image.INTERPOLATION_NONE);
475                    write();
476                    
477            }
478    
479            // Write an image to a file
480            private void doActionWrite() throws ApplicationException, IOException, ExpressionException {
481                    required("source", source);
482                    required("destination", destination);
483                    
484                    source.writeOut(destination,overwrite,quality);
485            }
486    
487            // Write an image to the browser
488            private void doActionWriteToBrowser() throws IOException, PageException {
489                    required("source", source);
490                    
491                    String path=null;
492                    
493                    // create destination
494                    if(!base64 || !StringUtil.isEmpty(name)) {
495                            path=touchDestination();
496                            write();
497                    }
498                    // link destination
499                    if(StringUtil.isEmpty(name))writeLink(path);
500                    
501                    
502                    
503                    
504            }
505    
506    
507    
508            private void required(String label, Object value) throws ApplicationException {
509                    if(value==null)
510                            throw new ApplicationException("Missing attribute ["+label+"]. The action ["+strAction+"] requires the attribute [" + label + "].");
511                            //throw new ApplicationException("missing attribute ["+label+"], for the action ["+strAction+"] this attribute is required but was not passed in");
512            }
513            private void required(String label, int value,int nullValue) throws ApplicationException {
514                    if(value==nullValue) 
515                            throw new ApplicationException("Missing attribute ["+label+"]. The action ["+strAction+"] requires the attribute [" + label + "].");
516                            //throw new ApplicationException("missing attribute ["+label+"], for the action ["+strAction+"] this attribute is required but was not passed in");
517            }
518    
519    
520            private void write() throws IOException, PageException {
521                    if(destination!=null) {
522                            doActionWrite();
523                    }
524                    if(!StringUtil.isEmpty(name)) {
525                            required("source", source);
526                            pageContext.setVariable(name, source);// TODO ist das so gut
527                    }
528                    //if(writeToResponseWhenNoOtherTarget) doActionWriteToBrowser();
529            }
530            
531    
532            class Info {
533                    Struct struct;
534                    private railo.runtime.img.Image img;
535    
536                    public Info(railo.runtime.img.Image img) {
537                            this.img=img;
538                    }
539    
540                    /**
541                     * @return the sct
542                     * @throws PageException 
543                     */
544                    public Struct getStruct() throws PageException {
545                            if(struct==null)
546                                    struct=doActionInfo(img);
547                            return struct;
548                    }
549            }
550    
551    
552            private int toDimension(String label, String dimension, Info info) throws PageException {
553                    if(StringUtil.isEmpty(dimension)) return -1;
554                    dimension=dimension.trim();
555                    // int value
556                    int i=Caster.toIntValue(dimension,-1);
557                    if(i>-1) return i;
558                    
559                    // percent value
560                    if(StringUtil.endsWith(dimension, '%')) {
561                            float pro=Caster.toIntValue(
562                                                    dimension.substring(0,dimension.length()-1).trim(),
563                                                    -1);
564                            if(pro<0 || pro>100) 
565                                    throw new ExpressionException("attribute ["+label+"] value has a invalid percent definition ["+dimension+"]"); 
566                            pro/=100F;
567                            return (int)(Caster.toFloatValue(info.getStruct().get(label))*pro);
568                    }
569                    throw new ExpressionException("attribute ["+label+"] value has a invalid definition ["+dimension+"]"); 
570                    
571            }
572    
573    }