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}