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 **/ 019/* 020* 021 022Licensed under the Apache License, Version 2.0 (the "License"); 023you may not use this file except in compliance with the License. 024You may obtain a copy of the License at 025 026 http://www.apache.org/licenses/LICENSE-2.0 027 028Unless required by applicable law or agreed to in writing, software 029distributed under the License is distributed on an "AS IS" BASIS, 030WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 031See the License for the specific language governing permissions and 032limitations under the License. 033*/ 034 035package lucee.runtime.img.filter;import java.awt.AlphaComposite; 036import java.awt.Graphics2D; 037import java.awt.Rectangle; 038import java.awt.geom.AffineTransform; 039import java.awt.geom.Point2D; 040import java.awt.geom.Rectangle2D; 041import java.awt.image.BandCombineOp; 042import java.awt.image.BufferedImage; 043import java.awt.image.ColorModel; 044 045import lucee.runtime.engine.ThreadLocalPageContext; 046import lucee.runtime.exp.FunctionException; 047import lucee.runtime.exp.PageException; 048import lucee.runtime.img.ImageUtil; 049import lucee.runtime.type.KeyImpl; 050import lucee.runtime.type.Struct; 051import lucee.runtime.type.util.CollectionUtil; 052 053/** 054 * A filter which draws a drop shadow based on the alpha channel of the image. 055 */ 056public class ShadowFilter extends AbstractBufferedImageOp implements DynFiltering { 057 058 private float radius = 5; 059 private float angle = (float)Math.PI*6/4; 060 private float distance = 5; 061 private float opacity = 0.5f; 062 private boolean addMargins = false; 063 private boolean shadowOnly = false; 064 private int shadowColor = 0xff000000; 065 066 /** 067 * Construct a ShadowFilter. 068 */ 069 public ShadowFilter() { 070 } 071 072 /** 073 * Construct a ShadowFilter. 074 * @param radius the radius of the shadow 075 * @param xOffset the X offset of the shadow 076 * @param yOffset the Y offset of the shadow 077 * @param opacity the opacity of the shadow 078 */ 079 public ShadowFilter(float radius, float xOffset, float yOffset, float opacity) { 080 this.radius = radius; 081 this.angle = (float)Math.atan2(yOffset, xOffset); 082 this.distance = (float)Math.sqrt(xOffset*xOffset + yOffset*yOffset); 083 this.opacity = opacity; 084 } 085 086 /** 087 * Specifies the angle of the shadow. 088 * @param angle the angle of the shadow. 089 * @angle 090 * @see #getAngle 091 */ 092 public void setAngle(float angle) { 093 this.angle = angle; 094 } 095 096 /** 097 * Returns the angle of the shadow. 098 * @return the angle of the shadow. 099 * @see #setAngle 100 */ 101 public float getAngle() { 102 return angle; 103 } 104 105 /** 106 * Set the distance of the shadow. 107 * @param distance the distance. 108 * @see #getDistance 109 */ 110 public void setDistance(float distance) { 111 this.distance = distance; 112 } 113 114 /** 115 * Get the distance of the shadow. 116 * @return the distance. 117 * @see #setDistance 118 */ 119 public float getDistance() { 120 return distance; 121 } 122 123 /** 124 * Set the radius of the kernel, and hence the amount of blur. The bigger the radius, the longer this filter will take. 125 * @param radius the radius of the blur in pixels. 126 * @see #getRadius 127 */ 128 public void setRadius(float radius) { 129 this.radius = radius; 130 } 131 132 /** 133 * Get the radius of the kernel. 134 * @return the radius 135 * @see #setRadius 136 */ 137 public float getRadius() { 138 return radius; 139 } 140 141 /** 142 * Set the opacity of the shadow. 143 * @param opacity the opacity. 144 * @see #getOpacity 145 */ 146 public void setOpacity(float opacity) { 147 this.opacity = opacity; 148 } 149 150 /** 151 * Get the opacity of the shadow. 152 * @return the opacity. 153 * @see #setOpacity 154 */ 155 public float getOpacity() { 156 return opacity; 157 } 158 159 /** 160 * Set the color of the shadow. 161 * @param shadowColor the color. 162 * @see #getShadowColor 163 */ 164 public void setShadowColor(int shadowColor) { 165 this.shadowColor = shadowColor; 166 } 167 168 /** 169 * Get the color of the shadow. 170 * @return the color. 171 * @see #setShadowColor 172 */ 173 public int getShadowColor() { 174 return shadowColor; 175 } 176 177 /** 178 * Set whether to increase the size of the output image to accomodate the shadow. 179 * @param addMargins true to add margins. 180 * @see #getAddMargins 181 */ 182 public void setAddMargins(boolean addMargins) { 183 this.addMargins = addMargins; 184 } 185 186 /** 187 * Get whether to increase the size of the output image to accomodate the shadow. 188 * @return true to add margins. 189 * @see #setAddMargins 190 */ 191 public boolean getAddMargins() { 192 return addMargins; 193 } 194 195 /** 196 * Set whether to only draw the shadow without the original image. 197 * @param shadowOnly true to only draw the shadow. 198 * @see #getShadowOnly 199 */ 200 public void setShadowOnly(boolean shadowOnly) { 201 this.shadowOnly = shadowOnly; 202 } 203 204 /** 205 * Get whether to only draw the shadow without the original image. 206 * @return true to only draw the shadow. 207 * @see #setShadowOnly 208 */ 209 public boolean getShadowOnly() { 210 return shadowOnly; 211 } 212 213 public Rectangle2D getBounds2D( BufferedImage src ) { 214 Rectangle r = new Rectangle(0, 0, src.getWidth(), src.getHeight()); 215 if ( addMargins ) { 216 float xOffset = distance*(float)Math.cos(angle); 217 float yOffset = -distance*(float)Math.sin(angle); 218 r.width += (int)(Math.abs(xOffset)+2*radius); 219 r.height += (int)(Math.abs(yOffset)+2*radius); 220 } 221 return r; 222 } 223 224 public Point2D getPoint2D( Point2D srcPt, Point2D dstPt ) { 225 if ( dstPt == null ) 226 dstPt = new Point2D.Double(); 227 228 if ( addMargins ) { 229 float xOffset = distance*(float)Math.cos(angle); 230 float yOffset = -distance*(float)Math.sin(angle); 231 float topShadow = Math.max( 0, radius-yOffset ); 232 float leftShadow = Math.max( 0, radius-xOffset ); 233 dstPt.setLocation( srcPt.getX()+leftShadow, srcPt.getY()+topShadow ); 234 } else 235 dstPt.setLocation( srcPt.getX(), srcPt.getY() ); 236 237 return dstPt; 238 } 239 240 public BufferedImage filter( BufferedImage src, BufferedImage dst ) { 241 int width = src.getWidth(); 242 int height = src.getHeight(); 243 244 if ( dst == null ) { 245 if ( addMargins ) { 246 ColorModel cm = src.getColorModel(); 247 dst = new BufferedImage(cm, cm.createCompatibleWritableRaster(src.getWidth(), src.getHeight()), cm.isAlphaPremultiplied(), null); 248 } else 249 dst = createCompatibleDestImage( src, null ); 250 } 251 252 float shadowR = ((shadowColor >> 16) & 0xff) / 255f; 253 float shadowG = ((shadowColor >> 8) & 0xff) / 255f; 254 float shadowB = (shadowColor & 0xff) / 255f; 255 256 // Make a black mask from the image's alpha channel 257 float[][] extractAlpha = { 258 { 0, 0, 0, shadowR }, 259 { 0, 0, 0, shadowG }, 260 { 0, 0, 0, shadowB }, 261 { 0, 0, 0, opacity } 262 }; 263 BufferedImage shadow = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); 264 new BandCombineOp( extractAlpha, null ).filter( src.getRaster(), shadow.getRaster() ); 265 shadow = new GaussianFilter( radius ).filter( shadow, (BufferedImage)null ); 266 267 float xOffset = distance*(float)Math.cos(angle); 268 float yOffset = -distance*(float)Math.sin(angle); 269 270 Graphics2D g = dst.createGraphics(); 271 g.setComposite( AlphaComposite.getInstance( AlphaComposite.SRC_OVER, opacity ) ); 272 if ( addMargins ) { 273 //float radius2 = radius/2; 274 float topShadow = Math.max( 0, radius-yOffset ); 275 float leftShadow = Math.max( 0, radius-xOffset ); 276 g.translate( topShadow, leftShadow ); 277 } 278 g.drawRenderedImage( shadow, AffineTransform.getTranslateInstance( xOffset, yOffset ) ); 279 if ( !shadowOnly ) { 280 g.setComposite( AlphaComposite.SrcOver ); 281 g.drawRenderedImage( src, null ); 282 } 283 g.dispose(); 284 285 return dst; 286 } 287 288 public String toString() { 289 return "Stylize/Drop Shadow..."; 290 } 291 public BufferedImage filter(BufferedImage src, Struct parameters) throws PageException {BufferedImage dst=ImageUtil.createBufferedImage(src); 292 Object o; 293 if((o=parameters.removeEL(KeyImpl.init("Radius")))!=null)setRadius(ImageFilterUtil.toFloatValue(o,"Radius")); 294 if((o=parameters.removeEL(KeyImpl.init("Angle")))!=null)setAngle(ImageFilterUtil.toFloatValue(o,"Angle")); 295 if((o=parameters.removeEL(KeyImpl.init("Distance")))!=null)setDistance(ImageFilterUtil.toFloatValue(o,"Distance")); 296 if((o=parameters.removeEL(KeyImpl.init("Opacity")))!=null)setOpacity(ImageFilterUtil.toFloatValue(o,"Opacity")); 297 if((o=parameters.removeEL(KeyImpl.init("ShadowColor")))!=null)setShadowColor(ImageFilterUtil.toColorRGB(o,"ShadowColor")); 298 if((o=parameters.removeEL(KeyImpl.init("AddMargins")))!=null)setAddMargins(ImageFilterUtil.toBooleanValue(o,"AddMargins")); 299 if((o=parameters.removeEL(KeyImpl.init("ShadowOnly")))!=null)setShadowOnly(ImageFilterUtil.toBooleanValue(o,"ShadowOnly")); 300 301 // check for arguments not supported 302 if(parameters.size()>0) { 303 throw new FunctionException(ThreadLocalPageContext.get(), "ImageFilter", 3, "parameters", "the parameter"+(parameters.size()>1?"s":"")+" ["+CollectionUtil.getKeyList(parameters,", ")+"] "+(parameters.size()>1?"are":"is")+" not allowed, only the following parameters are supported [Radius, Angle, Distance, Opacity, ShadowColor, AddMargins, ShadowOnly]"); 304 } 305 306 return filter(src, dst); 307 } 308}