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