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}