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.ColorModel;
041import java.awt.image.Kernel;
042
043import lucee.runtime.engine.ThreadLocalPageContext;
044import lucee.runtime.exp.ExpressionException;
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
052public abstract class ConvolveFilter extends AbstractBufferedImageOp  implements DynFiltering {
053        
054    /**
055     * Treat pixels off the edge as zero.
056     */
057        public final static int ZERO_EDGES = 0;
058
059    /**
060     * Clamp pixels off the edge to the nearest edge.
061     */
062        public final static int CLAMP_EDGES = 1;
063
064    /**
065     * Wrap pixels off the edge to the opposite edge.
066     */
067        public final static int WRAP_EDGES = 2;
068
069    /**
070     * The convolution kernel.
071     */
072        protected Kernel kernel = null;
073
074    /**
075     * Whether to convolve alpha.
076     */
077        protected boolean alpha = true;
078
079    /**
080     * Whether to promultiply the alpha before convolving.
081     */
082        protected boolean premultiplyAlpha = true;
083
084    /**
085     * What do do at the image edges.
086     */
087        private int edgeAction = CLAMP_EDGES;
088
089        /**
090         * Construct a filter with a null kernel. This is only useful if you're going to change the kernel later on.
091         */
092        public ConvolveFilter() {
093                this(new float[9]);
094        }
095
096        /**
097         * Construct a filter with the given 3x3 kernel.
098         * @param matrix an array of 9 floats containing the kernel
099         */
100        public ConvolveFilter(float[] matrix) {
101                this(new Kernel(3, 3, matrix));
102        }
103        
104        /**
105         * Construct a filter with the given kernel.
106         * @param rows  the number of rows in the kernel
107         * @param cols  the number of columns in the kernel
108         * @param matrix        an array of rows*cols floats containing the kernel
109         */
110        public ConvolveFilter(int rows, int cols, float[] matrix) {
111                this(new Kernel(cols, rows, matrix));
112        }
113        
114        /**
115         * Construct a filter with the given 3x3 kernel.
116         * @param kernel the convolution kernel
117         */
118        public ConvolveFilter(Kernel kernel) {
119                this.kernel = kernel;   
120        }
121
122    /**
123     * Set the convolution kernel.
124     * @param kernel the kernel
125     * @see #getKernel
126     */
127        public void setKernel(Kernel kernel) {
128                this.kernel = kernel;
129        }
130
131    /**
132     * Get the convolution kernel.
133     * @return the kernel
134     * @see #setKernel
135     */
136        public Kernel getKernel() {
137                return kernel;
138        }
139
140    /**
141     * Set the action to perfomr for pixels off the image edges.
142     * valid values are:
143     * - clamp (default): Clamp pixels off the edge to the nearest edge.
144     * - wrap: Wrap pixels off the edge to the opposite edge.
145     * - zero: Treat pixels off the edge as zero
146     * 
147     * @param edgeAction the action
148     * @throws ExpressionException 
149     */
150        public void setEdgeAction(String edgeAction) throws ExpressionException {
151                String str=edgeAction.trim().toUpperCase();
152                if("ZERO".equals(str)) this.edgeAction = ZERO_EDGES;
153                else if("CLAMP".equals(str)) this.edgeAction = CLAMP_EDGES;
154                else if("WRAP".equals(str)) this.edgeAction = WRAP_EDGES;
155                else 
156                        throw new ExpressionException("invalid value ["+edgeAction+"] for edgeAction, valid values are [clamp,wrap,zero]");
157        }
158
159    /**
160     * Get the action to perfomr for pixels off the image edges.
161     * @return the action
162     * @see #setEdgeAction
163     */
164        public int getEdgeAction() {
165                return edgeAction;
166        }
167
168    /**
169     * Set whether to convolve the alpha channel.
170     * @param useAlpha true to convolve the alpha
171     * @see #getUseAlpha
172     */
173        public void setUseAlpha( boolean useAlpha ) {
174                this.alpha = useAlpha;
175        }
176
177    /**
178     * Get whether to convolve the alpha channel.
179     * @return true to convolve the alpha
180     * @see #setUseAlpha
181     */
182        public boolean getUseAlpha() {
183                return alpha;
184        }
185
186    /**
187     * Set whether to premultiply the alpha channel.
188     * @param premultiplyAlpha true to premultiply the alpha
189     * @see #getPremultiplyAlpha
190     */
191        public void setPremultiplyAlpha( boolean premultiplyAlpha ) {
192                this.premultiplyAlpha = premultiplyAlpha;
193        }
194
195    /**
196     * Get whether to premultiply the alpha channel.
197     * @return true to premultiply the alpha
198     * @see #setPremultiplyAlpha
199     */
200        public boolean getPremultiplyAlpha() {
201                return premultiplyAlpha;
202        }
203
204    public BufferedImage filter( BufferedImage src, BufferedImage dst ) {
205        int width = src.getWidth();
206        int height = src.getHeight();
207
208        if ( dst == null )
209            dst = createCompatibleDestImage( src, null );
210
211        int[] inPixels = new int[width*height];
212        int[] outPixels = new int[width*height];
213        getRGB( src, 0, 0, width, height, inPixels );
214
215        if ( premultiplyAlpha )
216                        ImageMath.premultiply( inPixels, 0, inPixels.length );
217                convolve(kernel, inPixels, outPixels, width, height, alpha, edgeAction);
218        if ( premultiplyAlpha )
219                        ImageMath.unpremultiply( outPixels, 0, outPixels.length );
220
221        setRGB( dst, 0, 0, width, height, outPixels );
222        return dst;
223    }
224
225    public BufferedImage createCompatibleDestImage(BufferedImage src, ColorModel dstCM) {
226        if ( dstCM == null )
227            dstCM = src.getColorModel();
228        return new BufferedImage(dstCM, dstCM.createCompatibleWritableRaster(src.getWidth(), src.getHeight()), dstCM.isAlphaPremultiplied(), null);
229    }
230    
231    public Rectangle2D getBounds2D( BufferedImage src ) {
232        return new Rectangle(0, 0, src.getWidth(), src.getHeight());
233    }
234    
235    public Point2D getPoint2D( Point2D srcPt, Point2D dstPt ) {
236        if ( dstPt == null )
237            dstPt = new Point2D.Double();
238        dstPt.setLocation( srcPt.getX(), srcPt.getY() );
239        return dstPt;
240    }
241
242    public RenderingHints getRenderingHints() {
243        return null;
244    }
245
246    /**
247     * Convolve a block of pixels.
248     * @param kernel the kernel
249     * @param inPixels the input pixels
250     * @param outPixels the output pixels
251     * @param width the width
252     * @param height the height
253     * @param edgeAction what to do at the edges
254     */
255        public static void convolve(Kernel kernel, int[] inPixels, int[] outPixels, int width, int height, int edgeAction) {
256                convolve(kernel, inPixels, outPixels, width, height, true, edgeAction);
257        }
258        
259    /**
260     * Convolve a block of pixels.
261     * @param kernel the kernel
262     * @param inPixels the input pixels
263     * @param outPixels the output pixels
264     * @param width the width
265     * @param height the height
266     * @param alpha include alpha channel
267     * @param edgeAction what to do at the edges
268     */
269        public static void convolve(Kernel kernel, int[] inPixels, int[] outPixels, int width, int height, boolean alpha, int edgeAction) {
270                if (kernel.getHeight() == 1)
271                        convolveH(kernel, inPixels, outPixels, width, height, alpha, edgeAction);
272                else if (kernel.getWidth() == 1)
273                        convolveV(kernel, inPixels, outPixels, width, height, alpha, edgeAction);
274                else
275                        convolveHV(kernel, inPixels, outPixels, width, height, alpha, edgeAction);
276        }
277        
278        /**
279         * Convolve with a 2D kernel.
280     * @param kernel the kernel
281     * @param inPixels the input pixels
282     * @param outPixels the output pixels
283     * @param width the width
284     * @param height the height
285     * @param alpha include alpha channel
286     * @param edgeAction what to do at the edges
287         */
288        public static void convolveHV(Kernel kernel, int[] inPixels, int[] outPixels, int width, int height, boolean alpha, int edgeAction) {
289                int index = 0;
290                float[] matrix = kernel.getKernelData( null );
291                int rows = kernel.getHeight();
292                int cols = kernel.getWidth();
293                int rows2 = rows/2;
294                int cols2 = cols/2;
295
296                for (int y = 0; y < height; y++) {
297                        for (int x = 0; x < width; x++) {
298                                float r = 0, g = 0, b = 0, a = 0;
299
300                                for (int row = -rows2; row <= rows2; row++) {
301                                        int iy = y+row;
302                                        int ioffset;
303                                        if (0 <= iy && iy < height)
304                                                ioffset = iy*width;
305                                        else if ( edgeAction == CLAMP_EDGES )
306                                                ioffset = y*width;
307                                        else if ( edgeAction == WRAP_EDGES )
308                                                ioffset = ((iy+height) % height) * width;
309                                        else
310                                                continue;
311                                        int moffset = cols*(row+rows2)+cols2;
312                                        for (int col = -cols2; col <= cols2; col++) {
313                                                float f = matrix[moffset+col];
314
315                                                if (f != 0) {
316                                                        int ix = x+col;
317                                                        if (!(0 <= ix && ix < width)) {
318                                                                if ( edgeAction == CLAMP_EDGES )
319                                                                        ix = x;
320                                                                else if ( edgeAction == WRAP_EDGES )
321                                                                        ix = (x+width) % width;
322                                                                else
323                                                                        continue;
324                                                        }
325                                                        int rgb = inPixels[ioffset+ix];
326                                                        a += f * ((rgb >> 24) & 0xff);
327                                                        r += f * ((rgb >> 16) & 0xff);
328                                                        g += f * ((rgb >> 8) & 0xff);
329                                                        b += f * (rgb & 0xff);
330                                                }
331                                        }
332                                }
333                                int ia = alpha ? PixelUtils.clamp((int)(a+0.5)) : 0xff;
334                                int ir = PixelUtils.clamp((int)(r+0.5));
335                                int ig = PixelUtils.clamp((int)(g+0.5));
336                                int ib = PixelUtils.clamp((int)(b+0.5));
337                                outPixels[index++] = (ia << 24) | (ir << 16) | (ig << 8) | ib;
338                        }
339                }
340        }
341
342        /**
343         * Convolve with a kernel consisting of one row.
344     * @param kernel the kernel
345     * @param inPixels the input pixels
346     * @param outPixels the output pixels
347     * @param width the width
348     * @param height the height
349     * @param alpha include alpha channel
350     * @param edgeAction what to do at the edges
351         */
352        public static void convolveH(Kernel kernel, int[] inPixels, int[] outPixels, int width, int height, boolean alpha, int edgeAction) {
353                int index = 0;
354                float[] matrix = kernel.getKernelData( null );
355                int cols = kernel.getWidth();
356                int cols2 = cols/2;
357
358                for (int y = 0; y < height; y++) {
359                        int ioffset = y*width;
360                        for (int x = 0; x < width; x++) {
361                                float r = 0, g = 0, b = 0, a = 0;
362                                int moffset = cols2;
363                                for (int col = -cols2; col <= cols2; col++) {
364                                        float f = matrix[moffset+col];
365
366                                        if (f != 0) {
367                                                int ix = x+col;
368                                                if ( ix < 0 ) {
369                                                        if ( edgeAction == CLAMP_EDGES )
370                                                                ix = 0;
371                                                        else if ( edgeAction == WRAP_EDGES )
372                                                                ix = (x+width) % width;
373                                                } else if ( ix >= width) {
374                                                        if ( edgeAction == CLAMP_EDGES )
375                                                                ix = width-1;
376                                                        else if ( edgeAction == WRAP_EDGES )
377                                                                ix = (x+width) % width;
378                                                }
379                                                int rgb = inPixels[ioffset+ix];
380                                                a += f * ((rgb >> 24) & 0xff);
381                                                r += f * ((rgb >> 16) & 0xff);
382                                                g += f * ((rgb >> 8) & 0xff);
383                                                b += f * (rgb & 0xff);
384                                        }
385                                }
386                                int ia = alpha ? PixelUtils.clamp((int)(a+0.5)) : 0xff;
387                                int ir = PixelUtils.clamp((int)(r+0.5));
388                                int ig = PixelUtils.clamp((int)(g+0.5));
389                                int ib = PixelUtils.clamp((int)(b+0.5));
390                                outPixels[index++] = (ia << 24) | (ir << 16) | (ig << 8) | ib;
391                        }
392                }
393        }
394
395        /**
396         * Convolve with a kernel consisting of one column.
397     * @param kernel the kernel
398     * @param inPixels the input pixels
399     * @param outPixels the output pixels
400     * @param width the width
401     * @param height the height
402     * @param alpha include alpha channel
403     * @param edgeAction what to do at the edges
404         */
405        public static void convolveV(Kernel kernel, int[] inPixels, int[] outPixels, int width, int height, boolean alpha, int edgeAction) {
406                int index = 0;
407                float[] matrix = kernel.getKernelData( null );
408                int rows = kernel.getHeight();
409                int rows2 = rows/2;
410
411                for (int y = 0; y < height; y++) {
412                        for (int x = 0; x < width; x++) {
413                                float r = 0, g = 0, b = 0, a = 0;
414
415                                for (int row = -rows2; row <= rows2; row++) {
416                                        int iy = y+row;
417                                        int ioffset;
418                                        if ( iy < 0 ) {
419                                                if ( edgeAction == CLAMP_EDGES )
420                                                        ioffset = 0;
421                                                else if ( edgeAction == WRAP_EDGES )
422                                                        ioffset = ((y+height) % height)*width;
423                                                else
424                                                        ioffset = iy*width;
425                                        } else if ( iy >= height) {
426                                                if ( edgeAction == CLAMP_EDGES )
427                                                        ioffset = (height-1)*width;
428                                                else if ( edgeAction == WRAP_EDGES )
429                                                        ioffset = ((y+height) % height)*width;
430                                                else
431                                                        ioffset = iy*width;
432                                        } else
433                                                ioffset = iy*width;
434
435                                        float f = matrix[row+rows2];
436
437                                        if (f != 0) {
438                                                int rgb = inPixels[ioffset+x];
439                                                a += f * ((rgb >> 24) & 0xff);
440                                                r += f * ((rgb >> 16) & 0xff);
441                                                g += f * ((rgb >> 8) & 0xff);
442                                                b += f * (rgb & 0xff);
443                                        }
444                                }
445                                int ia = alpha ? PixelUtils.clamp((int)(a+0.5)) : 0xff;
446                                int ir = PixelUtils.clamp((int)(r+0.5));
447                                int ig = PixelUtils.clamp((int)(g+0.5));
448                                int ib = PixelUtils.clamp((int)(b+0.5));
449                                outPixels[index++] = (ia << 24) | (ir << 16) | (ig << 8) | ib;
450                        }
451                }
452        }
453
454        public String toString() {
455                return "Blur/Convolve...";
456        }
457        public BufferedImage filter(BufferedImage src, Struct parameters) throws PageException {BufferedImage dst=ImageUtil.createBufferedImage(src);
458                Object o;
459                if((o=parameters.removeEL(KeyImpl.init("EdgeAction")))!=null)setEdgeAction(ImageFilterUtil.toString(o,"EdgeAction"));
460                if((o=parameters.removeEL(KeyImpl.init("UseAlpha")))!=null)setUseAlpha(ImageFilterUtil.toBooleanValue(o,"UseAlpha"));
461                if((o=parameters.removeEL(KeyImpl.init("PremultiplyAlpha")))!=null)setPremultiplyAlpha(ImageFilterUtil.toBooleanValue(o,"PremultiplyAlpha"));
462
463                // check for arguments not supported
464                if(parameters.size()>0) {
465                        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 [Kernel, EdgeAction, UseAlpha, PremultiplyAlpha]");
466                }
467
468                return filter(src, dst);
469        }
470}