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