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.Rectangle;
036import java.awt.RenderingHints;
037import java.awt.geom.Point2D;
038import java.awt.geom.Rectangle2D;
039import java.awt.image.BufferedImage;
040import java.awt.image.BufferedImageOp;
041import java.awt.image.ColorModel;
042import java.util.Random;
043
044import lucee.runtime.engine.ThreadLocalPageContext;
045import lucee.runtime.exp.FunctionException;
046import lucee.runtime.exp.PageException;
047import lucee.runtime.img.ImageUtil;
048import lucee.runtime.type.KeyImpl;
049import lucee.runtime.type.Struct;
050import lucee.runtime.type.util.CollectionUtil;
051/**
052 * A filter which produces an image simulating brushed metal.
053 */
054public class BrushedMetalFilter implements BufferedImageOp, DynFiltering {
055
056        private int radius = 10;
057        private float amount = 0.1f;
058        private int color = 0xff888888;
059        private float shine = 0.1f;
060    private boolean monochrome = true;
061        private Random randomNumbers;
062
063    /**
064     * Constructs a BrushedMetalFilter object.
065     */
066    public BrushedMetalFilter() {
067    }
068    
069    /**
070     * Constructs a BrushedMetalFilter object.
071     *
072     * @param color       an int specifying the metal color
073     * @param radius      an int specifying the blur size
074     * @param amount      a float specifying the amount of texture
075     * @param monochrome  a boolean -- true for monochrome texture
076     * @param shine       a float specifying the shine to add
077     */
078    public BrushedMetalFilter( int color, int radius, float amount, boolean monochrome, float shine) {
079        this.color = color;
080        this.radius = radius;
081        this.amount = amount;
082        this.monochrome = monochrome;
083        this.shine = shine;
084    }
085    
086    public BufferedImage filter( BufferedImage src, BufferedImage dst ) {
087        int width = src.getWidth();
088        int height = src.getHeight();
089
090        if ( dst == null )
091            dst = createCompatibleDestImage( src, null );
092
093        int[] inPixels = new int[width];
094        int[] outPixels = new int[width];
095
096        randomNumbers = new Random(0);
097        int a = color & 0xff000000;
098        int r = (color >> 16) & 0xff;
099        int g = (color >> 8) & 0xff;
100        int b = color & 0xff;
101                for ( int y = 0; y < height; y++ ) {
102            for ( int x = 0; x < width; x++ ) {
103                int tr = r;
104                int tg = g;
105                int tb = b;
106                if ( shine != 0 ) {
107                    int f = (int)(255*shine*Math.sin( (double)x/width*Math.PI ));
108                    tr += f;
109                    tg += f;
110                    tb += f;
111                }
112                if (monochrome) {
113                    int n = (int)(255 * (2*randomNumbers.nextFloat() - 1) * amount);
114                    inPixels[x] = a | (clamp(tr+n) << 16) | (clamp(tg+n) << 8) | clamp(tb+n);
115                } else {
116                    inPixels[x] = a | (random(tr) << 16) | (random(tg) << 8) | random(tb);
117                }
118            }
119
120            if ( radius != 0 ) {
121                blur( inPixels, outPixels, width, radius );
122                setRGB( dst, 0, y, width, 1, outPixels );
123            } else
124                setRGB( dst, 0, y, width, 1, inPixels );
125        }
126        return dst;
127    }
128
129        private int random(int x) {
130                x += (int)(255*(2*randomNumbers.nextFloat() - 1) * amount);
131                if (x < 0)
132                        x = 0;
133                else if (x > 0xff)
134                        x = 0xff;
135                return x;
136        }
137
138        private static int clamp(int c) {
139                if (c < 0)
140                        return 0;
141                if (c > 255)
142                        return 255;
143                return c;
144        }
145
146        /**
147         * Return a mod b. This differs from the % operator with respect to negative numbers.
148         * @param a the dividend
149         * @param b the divisor
150         * @return a mod b
151         */
152        private static int mod(int a, int b) {
153                int n = a/b;
154                
155                a -= n*b;
156                if (a < 0)
157                        return a + b;
158                return a;
159        }
160
161    public void blur( int[] in, int[] out, int width, int radius ) {
162        int widthMinus1 = width-1;
163        int r2 = 2*radius+1;
164        int tr = 0, tg = 0, tb = 0;
165
166        for ( int i = -radius; i <= radius; i++ ) {
167            int rgb = in[mod(i, width)];
168            tr += (rgb >> 16) & 0xff;
169            tg += (rgb >> 8) & 0xff;
170            tb += rgb & 0xff;
171        }
172
173        for ( int x = 0; x < width; x++ ) {
174            out[x] = 0xff000000 | ((tr/r2) << 16) | ((tg/r2) << 8) | (tb/r2);
175
176            int i1 = x+radius+1;
177            if ( i1 > widthMinus1 )
178                i1 = mod( i1, width );
179            int i2 = x-radius;
180            if ( i2 < 0 )
181                i2 = mod( i2, width );
182            int rgb1 = in[i1];
183            int rgb2 = in[i2];
184            
185            tr += ((rgb1 & 0xff0000)-(rgb2 & 0xff0000)) >> 16;
186            tg += ((rgb1 & 0xff00)-(rgb2 & 0xff00)) >> 8;
187            tb += (rgb1 & 0xff)-(rgb2 & 0xff);
188        }
189    }
190
191        /**
192         * Set the horizontal size of the blur.
193         * @param radius the radius of the blur in the horizontal direction
194     * @min-value 0
195     * @max-value 100+
196     * @see #getRadius
197         */
198        public void setRadius(int radius) {
199                this.radius = radius;
200        }
201        
202        /**
203         * Get the horizontal size of the blur.
204         * @return the radius of the blur in the horizontal direction
205     * @see #setRadius
206         */
207        public int getRadius() {
208                return radius;
209        }
210        
211        /**
212         * Set the amount of noise to add in the range 0..1.
213         * @param amount the amount of noise
214     * @min-value 0
215     * @max-value 1
216     * @see #getAmount
217         */
218        public void setAmount(float amount) {
219                this.amount = amount;
220        }
221        
222        /**
223         * Get the amount of noise to add.
224         * @return the amount of noise
225     * @see #setAmount
226         */
227        public float getAmount() {
228                return amount;
229        }
230
231        /**
232         * Set the amount of shine to add to the range 0..1.
233         * @param shine the amount of shine
234     * @min-value 0
235     * @max-value 1
236     * @see #getShine
237         */
238        public void setShine( float shine ) {
239                this.shine = shine;
240        }
241        
242        /**
243         * Get the amount of shine to add in the range 0..1.
244         * @return the amount of shine
245     * @see #setShine
246         */
247        public float getShine() {
248                return shine;
249        }
250
251        /**
252         * Set the color of the metal.
253         * @param color the color in ARGB form
254     * @see #getColor
255         */
256        public void setColor(int color) {
257                this.color = color;
258        }
259        
260        /**
261         * Get the color of the metal.
262         * @return the color in ARGB form
263     * @see #setColor
264         */
265        public int getColor() {
266                return color;
267        }
268        
269        /**
270         * Set the type of noise to add.
271         * @param monochrome true for monochrome noise
272     * @see #getMonochrome
273         */
274        public void setMonochrome(boolean monochrome) {
275                this.monochrome = monochrome;
276        }
277        
278        /**
279         * Get the type of noise to add.
280         * @return true for monochrome noise
281     * @see #setMonochrome
282         */
283        public boolean getMonochrome() {
284                return monochrome;
285        }
286        
287    public BufferedImage createCompatibleDestImage(BufferedImage src, ColorModel dstCM) {
288        if ( dstCM == null )
289            dstCM = src.getColorModel();
290        return new BufferedImage(dstCM, dstCM.createCompatibleWritableRaster(src.getWidth(), src.getHeight()), dstCM.isAlphaPremultiplied(), null);
291    }
292    
293    public Rectangle2D getBounds2D( BufferedImage src ) {
294        return new Rectangle(0, 0, src.getWidth(), src.getHeight());
295    }
296    
297    public Point2D getPoint2D( Point2D srcPt, Point2D dstPt ) {
298        if ( dstPt == null )
299            dstPt = new Point2D.Double();
300        dstPt.setLocation( srcPt.getX(), srcPt.getY() );
301        return dstPt;
302    }
303
304    public RenderingHints getRenderingHints() {
305        return null;
306    }
307
308        /**
309         * A convenience method for setting ARGB pixels in an image. This tries to avoid the performance
310         * penalty of BufferedImage.setRGB unmanaging the image.
311         */
312        private void setRGB( BufferedImage image, int x, int y, int width, int height, int[] pixels ) {
313                int type = image.getType();
314                if ( type == BufferedImage.TYPE_INT_ARGB || type == BufferedImage.TYPE_INT_RGB )
315                        image.getRaster().setDataElements( x, y, width, height, pixels );
316                else
317                        image.setRGB( x, y, width, height, pixels, 0, width );
318    }
319
320        public String toString() {
321                return "Texture/Brushed Metal...";
322        }
323        public BufferedImage filter(BufferedImage src, Struct parameters) throws PageException {BufferedImage dst=ImageUtil.createBufferedImage(src);
324                Object o;
325                if((o=parameters.removeEL(KeyImpl.init("Radius")))!=null)setRadius(ImageFilterUtil.toIntValue(o,"Radius"));
326                if((o=parameters.removeEL(KeyImpl.init("Amount")))!=null)setAmount(ImageFilterUtil.toFloatValue(o,"Amount"));
327                if((o=parameters.removeEL(KeyImpl.init("Shine")))!=null)setShine(ImageFilterUtil.toFloatValue(o,"Shine"));
328                if((o=parameters.removeEL(KeyImpl.init("Monochrome")))!=null)setMonochrome(ImageFilterUtil.toBooleanValue(o,"Monochrome"));
329                if((o=parameters.removeEL(KeyImpl.init("Color")))!=null)setColor(ImageFilterUtil.toIntValue(o,"Color"));
330
331                // check for arguments not supported
332                if(parameters.size()>0) {
333                        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, Amount, Shine, Monochrome, Color]");
334                }
335
336                return filter(src, dst);
337        }
338}