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