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.image.BufferedImage;
037
038import lucee.runtime.engine.ThreadLocalPageContext;
039import lucee.runtime.exp.ExpressionException;
040import lucee.runtime.exp.FunctionException;
041import lucee.runtime.exp.PageException;
042import lucee.runtime.img.ImageUtil;
043import lucee.runtime.type.KeyImpl;
044import lucee.runtime.type.Struct;
045import lucee.runtime.type.util.CollectionUtil;
046
047/**
048 * An abstract superclass for filters which distort images in some way. The subclass only needs to override
049 * two methods to provide the mapping between source and destination pixels.
050 */
051public abstract class TransformFilter extends AbstractBufferedImageOp  implements DynFiltering {
052        
053    
054
055    /**
056     * Use nearest-neighbour interpolation.
057     */
058        public final static int NEAREST_NEIGHBOUR = 0;
059
060    /**
061     * Use bilinear interpolation.
062     */
063        public final static int BILINEAR = 1;
064
065    /**
066     * The action to take for pixels off the image edge.
067     */
068        protected int edgeAction = ConvolveFilter.ZERO_EDGES;
069
070    /**
071     * The type of interpolation to use.
072     */
073        protected int interpolation = BILINEAR;
074
075    /**
076     * The output image rectangle.
077     */
078        protected Rectangle transformedSpace;
079
080    /**
081     * The input image rectangle.
082     */
083        protected Rectangle originalSpace;
084
085
086        public TransformFilter(){
087        }
088        public TransformFilter(int edgeAction){
089                this.edgeAction=edgeAction;
090        }
091        
092    /**
093     * Set the action to perfomr for pixels off the image edges.
094     * valid values are:
095     * - clamp (default): Clamp pixels off the edge to the nearest edge.
096     * - wrap: Wrap pixels off the edge to the opposite edge.
097     * - zero: Treat pixels off the edge as zero
098     * 
099     * @param edgeAction the action
100     * @throws ExpressionException 
101     */
102        public void setEdgeAction(String edgeAction) throws ExpressionException {
103                String str=edgeAction.trim().toUpperCase();
104                if("ZERO".equals(str)) this.edgeAction = ConvolveFilter.ZERO_EDGES;
105                else if("CLAMP".equals(str)) this.edgeAction = ConvolveFilter.CLAMP_EDGES;
106                else if("WRAP".equals(str)) this.edgeAction = ConvolveFilter.WRAP_EDGES;
107                else 
108                        throw new ExpressionException("invalid value ["+edgeAction+"] for edgeAction, valid values are [clamp,wrap,zero]");
109        }
110
111
112    /**
113     * Get the action to perform for pixels off the edge of the image.
114     * @return one of ZERO, CLAMP or WRAP
115     * @see #setEdgeAction
116     */
117        public int getEdgeAction() {
118                return edgeAction;
119        }
120        
121    /**
122     * Set the type of interpolation to perform.
123     * valid values are:
124     * - bilinear (default): Use bilinear interpolation.
125     * - nearest_neighbour: Use nearest-neighbour interpolation.
126     * 
127     * @param interpolation one of NEAREST_NEIGHBOUR or BILINEAR
128     * @see #getInterpolation
129     */
130        public void setInterpolation(String interpolation) throws ExpressionException {
131                String str=interpolation.trim().toUpperCase();
132                if("NEAREST_NEIGHBOUR".equals(str)) this.interpolation = NEAREST_NEIGHBOUR;
133                else if("BILINEAR".equals(str)) this.interpolation = BILINEAR;
134                else 
135                        throw new ExpressionException("invalid value ["+interpolation+"] for interpolation, valid values are [bilinear,nearest_neighbour]");
136        }
137
138    /**
139     * Get the type of interpolation to perform.
140     * @return one of NEAREST_NEIGHBOUR or BILINEAR
141     * @see #setInterpolation
142     */
143        public int getInterpolation() {
144                return interpolation;
145        }
146        
147    /**
148     * Inverse transform a point. This method needs to be overriden by all subclasses.
149     * @param x the X position of the pixel in the output image
150     * @param y the Y position of the pixel in the output image
151     * @param out the position of the pixel in the input image
152     */
153        protected abstract void transformInverse(int x, int y, float[] out);
154
155    /**
156     * Forward transform a rectangle. Used to determine the size of the output image.
157     * @param rect the rectangle to transform
158     */
159        protected void transformSpace(Rectangle rect) {
160        }
161
162    public BufferedImage filter( BufferedImage src, BufferedImage dst ) {
163        
164        
165        
166        int width = src.getWidth();
167        int height = src.getHeight();
168        
169        
170        
171        
172        
173                //int type = src.getType();
174                //WritableRaster srcRaster = 
175        src.getRaster();
176
177                originalSpace = new Rectangle(0, 0, width, height);
178                transformedSpace = new Rectangle(0, 0, width, height);
179                transformSpace(transformedSpace);
180                
181                
182
183        if (dst == null ) {
184                dst=ImageUtil.createBufferedImage(src,transformedSpace.width,transformedSpace.height);
185        }
186        
187        //WritableRaster dstRaster = 
188        dst.getRaster();
189
190                int[] inPixels = getRGB( src, 0, 0, width, height, null );
191
192                if ( interpolation == NEAREST_NEIGHBOUR )
193                        return filterPixelsNN( dst, width, height, inPixels, transformedSpace );
194
195                int srcWidth = width;
196                int srcHeight = height;
197                int srcWidth1 = width-1;
198                int srcHeight1 = height-1;
199                int outWidth = transformedSpace.width;
200                int outHeight = transformedSpace.height;
201                int outX, outY;
202                //int index = 0;
203                int[] outPixels = new int[outWidth];
204
205                outX = transformedSpace.x;
206                outY = transformedSpace.y;
207                float[] out = new float[2];
208
209                for (int y = 0; y < outHeight; y++) {
210                        for (int x = 0; x < outWidth; x++) {
211                                transformInverse(outX+x, outY+y, out);
212                                int srcX = (int)Math.floor( out[0] );
213                                int srcY = (int)Math.floor( out[1] );
214                                float xWeight = out[0]-srcX;
215                                float yWeight = out[1]-srcY;
216                                int nw, ne, sw, se;
217
218                                if ( srcX >= 0 && srcX < srcWidth1 && srcY >= 0 && srcY < srcHeight1) {
219                                        // Easy case, all corners are in the image
220                                        int i = srcWidth*srcY + srcX;
221                                        nw = inPixels[i];
222                                        ne = inPixels[i+1];
223                                        sw = inPixels[i+srcWidth];
224                                        se = inPixels[i+srcWidth+1];
225                                } else {
226                                        // Some of the corners are off the image
227                                        nw = getPixel( inPixels, srcX, srcY, srcWidth, srcHeight );
228                                        ne = getPixel( inPixels, srcX+1, srcY, srcWidth, srcHeight );
229                                        sw = getPixel( inPixels, srcX, srcY+1, srcWidth, srcHeight );
230                                        se = getPixel( inPixels, srcX+1, srcY+1, srcWidth, srcHeight );
231                                }
232                                outPixels[x] = ImageMath.bilinearInterpolate(xWeight, yWeight, nw, ne, sw, se);
233                        }
234                        setRGB( dst, 0, y, transformedSpace.width, 1, outPixels );
235                }
236                return dst;
237        }
238
239        final private int getPixel( int[] pixels, int x, int y, int width, int height ) {
240                if (x < 0 || x >= width || y < 0 || y >= height) {
241                        switch (edgeAction) {
242                        case ConvolveFilter.ZERO_EDGES:
243                        default:
244                                return 0;
245                        case ConvolveFilter.WRAP_EDGES:
246                                return pixels[(ImageMath.mod(y, height) * width) + ImageMath.mod(x, width)];
247                        case ConvolveFilter.CLAMP_EDGES:
248                                return pixels[(ImageMath.clamp(y, 0, height-1) * width) + ImageMath.clamp(x, 0, width-1)];
249                        }
250                }
251                return pixels[ y*width+x ];
252        }
253
254        protected BufferedImage filterPixelsNN( BufferedImage dst, int width, int height, int[] inPixels, Rectangle transformedSpace ) {
255                int srcWidth = width;
256                int srcHeight = height;
257                int outWidth = transformedSpace.width;
258                int outHeight = transformedSpace.height;
259                int outX, outY, srcX, srcY;
260                int[] outPixels = new int[outWidth];
261
262                outX = transformedSpace.x;
263                outY = transformedSpace.y;
264                int[] rgb = new int[4];
265                float[] out = new float[2];
266
267                for (int y = 0; y < outHeight; y++) {
268                        for (int x = 0; x < outWidth; x++) {
269                                transformInverse(outX+x, outY+y, out);
270                                srcX = (int)out[0];
271                                srcY = (int)out[1];
272                                // int casting rounds towards zero, so we check out[0] < 0, not srcX < 0
273                                if (out[0] < 0 || srcX >= srcWidth || out[1] < 0 || srcY >= srcHeight) {
274                                        int p;
275                                        switch (edgeAction) {
276                                        case ConvolveFilter.ZERO_EDGES:
277                                        default:
278                                                p = 0;
279                                                break;
280                                        case ConvolveFilter.WRAP_EDGES:
281                                                p = inPixels[(ImageMath.mod(srcY, srcHeight) * srcWidth) + ImageMath.mod(srcX, srcWidth)];
282                                                break;
283                                        case ConvolveFilter.CLAMP_EDGES:
284                                                p = inPixels[(ImageMath.clamp(srcY, 0, srcHeight-1) * srcWidth) + ImageMath.clamp(srcX, 0, srcWidth-1)];
285                                                break;
286                                        }
287                                        outPixels[x] = p;
288                                } else {
289                                        int i = srcWidth*srcY + srcX;
290                                        rgb[0] = inPixels[i];
291                                        outPixels[x] = inPixels[i];
292                                }
293                        }
294                        setRGB( dst, 0, y, transformedSpace.width, 1, outPixels );
295                }
296                return dst;
297        }
298        
299        public BufferedImage filter(BufferedImage src, Struct parameters) throws PageException {BufferedImage dst=ImageUtil.createBufferedImage(src);
300                Object o;
301                if((o=parameters.removeEL(KeyImpl.init("EdgeAction")))!=null)setEdgeAction(ImageFilterUtil.toString(o,"EdgeAction"));
302                if((o=parameters.removeEL(KeyImpl.init("Interpolation")))!=null)setInterpolation(ImageFilterUtil.toString(o,"Interpolation"));
303
304                // check for arguments not supported
305                if(parameters.size()>0) {
306                        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 [EdgeAction, Interpolation]");
307                }
308
309                return filter(src, dst);
310        }
311}
312