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.image.BufferedImage;
036
037import lucee.runtime.engine.ThreadLocalPageContext;
038import lucee.runtime.exp.FunctionException;
039import lucee.runtime.exp.PageException;
040import lucee.runtime.img.ImageUtil;
041import lucee.runtime.img.math.FFT;
042import lucee.runtime.type.KeyImpl;
043import lucee.runtime.type.Struct;
044import lucee.runtime.type.util.CollectionUtil;
045
046/**
047 * A filter which use FFTs to simulate lens blur on an image.
048 */
049public class LensBlurFilter extends AbstractBufferedImageOp  implements DynFiltering {
050    
051    private float radius = 10;
052        private float bloom = 2;
053        private float bloomThreshold = 192;
054    private float angle = 0;
055        private int sides = 5;
056
057        /**
058         * Set the radius of the kernel, and hence the amount of blur.
059         * @param radius the radius of the blur in pixels.
060     * @see #getRadius
061         */
062        public void setRadius(float radius) {
063                this.radius = radius;
064        }
065        
066        /**
067         * Get the radius of the kernel.
068         * @return the radius
069     * @see #setRadius
070         */
071        public float getRadius() {
072                return radius;
073        }
074
075        /**
076         * Set the number of sides of the aperture.
077         * @param sides the number of sides
078     * @see #getSides
079         */
080        public void setSides(int sides) {
081                this.sides = sides;
082        }
083        
084        /**
085         * Get the number of sides of the aperture.
086         * @return the number of sides
087     * @see #setSides
088         */
089        public int getSides() {
090                return sides;
091        }
092
093        /**
094         * Set the bloom factor.
095         * @param bloom the bloom factor
096     * @see #getBloom
097         */
098        public void setBloom(float bloom) {
099                this.bloom = bloom;
100        }
101        
102        /**
103         * Get the bloom factor.
104         * @return the bloom factor
105     * @see #setBloom
106         */
107        public float getBloom() {
108                return bloom;
109        }
110
111        /**
112         * Set the bloom threshold.
113         * @param bloomThreshold the bloom threshold
114     * @see #getBloomThreshold
115         */
116        public void setBloomThreshold(float bloomThreshold) {
117                this.bloomThreshold = bloomThreshold;
118        }
119        
120        /**
121         * Get the bloom threshold.
122         * @return the bloom threshold
123     * @see #setBloomThreshold
124         */
125        public float getBloomThreshold() {
126                return bloomThreshold;
127        }
128
129
130    public BufferedImage filter( BufferedImage src, BufferedImage dst ) {
131        int width = src.getWidth();
132        int height = src.getHeight();
133        int rows = 1, cols = 1;
134        int log2rows = 0, log2cols = 0;
135        int iradius = (int)Math.ceil(radius);
136        int tileWidth = 128;
137        int tileHeight = tileWidth;
138
139        //int adjustedWidth =(width + iradius*2);
140        //int adjustedHeight =(height + iradius*2);
141
142                tileWidth = iradius < 32 ? Math.min(128, width+2*iradius) : Math.min(256, width+2*iradius);
143                tileHeight = iradius < 32 ? Math.min(128, height+2*iradius) : Math.min(256, height+2*iradius);
144
145        if ( dst == null )
146            dst = new BufferedImage( width, height, BufferedImage.TYPE_INT_ARGB );
147
148        while (rows < tileHeight) {
149            rows *= 2;
150            log2rows++;
151        }
152        while (cols < tileWidth) {
153            cols *= 2;
154            log2cols++;
155        }
156        int w = cols;
157        int h = rows;
158
159                tileWidth = w;
160                tileHeight = h;//FIXME-tileWidth, w, and cols are always all the same
161
162        FFT fft = new FFT( Math.max(log2rows, log2cols) );
163
164        int[] rgb = new int[w*h];
165        float[][] mask = new float[2][w*h];
166        float[][] gb = new float[2][w*h];
167        float[][] ar = new float[2][w*h];
168
169        // Create the kernel
170                double polyAngle = Math.PI/sides;
171                double polyScale = 1.0f / Math.cos(polyAngle);
172                double r2 = radius*radius;
173                double rangle = Math.toRadians(angle);
174                float total = 0;
175        int i = 0;
176        for ( int y = 0; y < h; y++ ) {
177            for ( int x = 0; x < w; x++ ) {
178                double dx = x-w/2f;
179                double dy = y-h/2f;
180                                double r = dx*dx+dy*dy;
181                                double f = r < r2 ? 1 : 0;
182                                if (f != 0) {
183                                        r = Math.sqrt(r);
184                                        if ( sides != 0 ) {
185                                                double a = Math.atan2(dy, dx)+rangle;
186                                                a = ImageMath.mod(a, polyAngle*2)-polyAngle;
187                                                f = Math.cos(a) * polyScale;
188                                        } else
189                                                f = 1;
190                                        f = f*r < radius ? 1 : 0;
191                                }
192                                total += (float)f;
193
194                                mask[0][i] = (float)f;
195                mask[1][i] = 0;
196                i++;
197            }
198        }
199                
200        // Normalize the kernel
201        i = 0;
202        for ( int y = 0; y < h; y++ ) {
203            for ( int x = 0; x < w; x++ ) {
204                mask[0][i] /= total;
205                i++;
206            }
207        }
208
209        fft.transform2D( mask[0], mask[1], w, h, true );
210
211        for ( int tileY = -iradius; tileY < height; tileY += tileHeight-2*iradius ) {
212            for ( int tileX = -iradius; tileX < width; tileX += tileWidth-2*iradius ) {
213//                System.out.println("Tile: "+tileX+" "+tileY+" "+tileWidth+" "+tileHeight);
214
215                // Clip the tile to the image bounds
216                int tx = tileX, ty = tileY, tw = tileWidth, th = tileHeight;
217                int fx = 0, fy = 0;
218                if ( tx < 0 ) {
219                    tw += tx;
220                    fx -= tx;
221                    tx = 0;
222                }
223                if ( ty < 0 ) {
224                    th += ty;
225                    fy -= ty;
226                    ty = 0;
227                }
228                if ( tx+tw > width )
229                    tw = width-tx;
230                if ( ty+th > height )
231                    th = height-ty;
232                src.getRGB( tx, ty, tw, th, rgb, fy*w+fx, w );
233
234                // Create a float array from the pixels. Any pixels off the edge of the source image get duplicated from the edge.
235                i = 0;
236                for ( int y = 0; y < h; y++ ) {
237                    int imageY = y+tileY;
238                    int j;
239                    if ( imageY < 0 )
240                        j = fy;
241                    else if ( imageY > height )
242                        j = fy+th-1;
243                    else
244                        j = y;
245                    j *= w;
246                    for ( int x = 0; x < w; x++ ) {
247                        int imageX = x+tileX;
248                        int k;
249                        if ( imageX < 0 )
250                            k = fx;
251                        else if ( imageX > width )
252                            k = fx+tw-1;
253                        else
254                            k = x;
255                        k += j;
256
257                        ar[0][i] = ((rgb[k] >> 24) & 0xff);
258                        float r = ((rgb[k] >> 16) & 0xff);
259                        float g = ((rgb[k] >> 8) & 0xff);
260                        float b = (rgb[k] & 0xff);
261
262                                                // Bloom...
263                        if ( r > bloomThreshold )
264                                                        r *= bloom;
265//                                                      r = bloomThreshold + (r-bloomThreshold) * bloom;
266                        if ( g > bloomThreshold )
267                                                        g *= bloom;
268//                                                      g = bloomThreshold + (g-bloomThreshold) * bloom;
269                        if ( b > bloomThreshold )
270                                                        b *= bloom;
271//                                                      b = bloomThreshold + (b-bloomThreshold) * bloom;
272
273                                                ar[1][i] = r;
274                                                gb[0][i] = g;
275                                                gb[1][i] = b;
276
277                        i++;
278                        k++;
279                    }
280                }
281
282                // Transform into frequency space
283                fft.transform2D( ar[0], ar[1], cols, rows, true );
284                fft.transform2D( gb[0], gb[1], cols, rows, true );
285
286                // Multiply the transformed pixels by the transformed kernel
287                i = 0;
288                for ( int y = 0; y < h; y++ ) {
289                    for ( int x = 0; x < w; x++ ) {
290                        float re = ar[0][i];
291                        float im = ar[1][i];
292                        float rem = mask[0][i];
293                        float imm = mask[1][i];
294                        ar[0][i] = re*rem-im*imm;
295                        ar[1][i] = re*imm+im*rem;
296                        
297                        re = gb[0][i];
298                        im = gb[1][i];
299                        gb[0][i] = re*rem-im*imm;
300                        gb[1][i] = re*imm+im*rem;
301                        i++;
302                    }
303                }
304
305                // Transform back
306                fft.transform2D( ar[0], ar[1], cols, rows, false );
307                fft.transform2D( gb[0], gb[1], cols, rows, false );
308
309                // Convert back to RGB pixels, with quadrant remapping
310                int row_flip = w >> 1;
311                int col_flip = h >> 1;
312                int index = 0;
313
314                //FIXME-don't bother converting pixels off image edges
315                for ( int y = 0; y < w; y++ ) {
316                    int ym = y ^ row_flip;
317                    int yi = ym*cols;
318                    for ( int x = 0; x < w; x++ ) {
319                        int xm = yi + (x ^ col_flip);
320                        int a = (int)ar[0][xm];
321                        int r = (int)ar[1][xm];
322                        int g = (int)gb[0][xm];
323                        int b = (int)gb[1][xm];
324
325                                                // Clamp high pixels due to blooming
326                                                if ( r > 255 )
327                                                        r = 255;
328                                                if ( g > 255 )
329                                                        g = 255;
330                                                if ( b > 255 )
331                                                        b = 255;
332                        int argb = (a << 24) | (r << 16) | (g << 8) | b;
333                        rgb[index++] = argb;
334                    }
335                }
336
337                // Clip to the output image
338                tx = tileX+iradius;
339                ty = tileY+iradius;
340                tw = tileWidth-2*iradius;
341                th = tileHeight-2*iradius;
342                if ( tx+tw > width )
343                    tw = width-tx;
344                if ( ty+th > height )
345                    th = height-ty;
346                dst.setRGB( tx, ty, tw, th, rgb, iradius*w+iradius, w );
347            }
348        }
349        return dst;
350    }
351
352        public String toString() {
353                return "Blur/Lens Blur...";
354        }
355        public BufferedImage filter(BufferedImage src, Struct parameters) throws PageException {BufferedImage dst=ImageUtil.createBufferedImage(src);
356                Object o;
357                if((o=parameters.removeEL(KeyImpl.init("Radius")))!=null)setRadius(ImageFilterUtil.toFloatValue(o,"Radius"));
358                if((o=parameters.removeEL(KeyImpl.init("Sides")))!=null)setSides(ImageFilterUtil.toIntValue(o,"Sides"));
359                if((o=parameters.removeEL(KeyImpl.init("Bloom")))!=null)setBloom(ImageFilterUtil.toFloatValue(o,"Bloom"));
360                if((o=parameters.removeEL(KeyImpl.init("BloomThreshold")))!=null)setBloomThreshold(ImageFilterUtil.toFloatValue(o,"BloomThreshold"));
361
362                // check for arguments not supported
363                if(parameters.size()>0) {
364                        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, Sides, Bloom, BloomThreshold]");
365                }
366
367                return filter(src, dst);
368        }
369}