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