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;
037import java.awt.image.ColorModel;
038
039import lucee.runtime.engine.ThreadLocalPageContext;
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 * A page curl effect.
049 */
050public final class CurlFilter extends TransformFilter  implements DynFiltering {
051        
052        private float angle = 0;
053        private float transition = 0.0f;
054
055        private float width;
056        private float height;
057        private float radius;
058
059        /**
060         * Construct a CurlFilter with no distortion.
061         */
062        public CurlFilter() {
063                super(ConvolveFilter.ZERO_EDGES );
064        }
065
066        public void setTransition( float transition ) {
067                this.transition = transition;
068        }
069        
070        public float getTransition() {
071                return transition;
072        }
073        
074        public void setAngle(float angle) {
075                this.angle = angle;
076        }
077        
078        public float getAngle() {
079                return angle;
080        }
081        
082        public void setRadius( float radius ) {
083                this.radius = radius;
084        }
085
086        public float getRadius() {
087                return radius;
088        }
089        
090/*
091    public BufferedImage filter( BufferedImage src, BufferedImage dst ) {
092                this.width = src.getWidth();
093                this.height = src.getHeight();
094                return super.filter( src, dst );
095        }
096*/
097
098        static class Sampler {
099                private int edgeAction;
100                private int width, height;
101                private int[] inPixels;
102                
103                public Sampler( BufferedImage image ) {
104                        int width = image.getWidth();
105                        int height = image.getHeight();
106                        //int type = image.getType();
107                        inPixels = ImageUtils.getRGB( image, 0, 0, width, height, null );
108                }
109                
110                public int sample( float x, float y ) {
111                        int srcX = (int)Math.floor( x );
112                        int srcY = (int)Math.floor( y );
113                        float xWeight = x-srcX;
114                        float yWeight = y-srcY;
115                        int nw, ne, sw, se;
116
117                        if ( srcX >= 0 && srcX < width-1 && srcY >= 0 && srcY < height-1) {
118                                // Easy case, all corners are in the image
119                                int i = width*srcY + srcX;
120                                nw = inPixels[i];
121                                ne = inPixels[i+1];
122                                sw = inPixels[i+width];
123                                se = inPixels[i+width+1];
124                        } else {
125                                // Some of the corners are off the image
126                                nw = getPixel( inPixels, srcX, srcY, width, height );
127                                ne = getPixel( inPixels, srcX+1, srcY, width, height );
128                                sw = getPixel( inPixels, srcX, srcY+1, width, height );
129                                se = getPixel( inPixels, srcX+1, srcY+1, width, height );
130                        }
131                        return ImageMath.bilinearInterpolate(xWeight, yWeight, nw, ne, sw, se);
132                }
133
134                final private int getPixel( int[] pixels, int x, int y, int width, int height ) {
135                        if (x < 0 || x >= width || y < 0 || y >= height) {
136                                switch (edgeAction) {
137                                case ConvolveFilter.ZERO_EDGES:
138                                default:
139                                        return 0;
140                                case ConvolveFilter.WRAP_EDGES:
141                                        return pixels[(ImageMath.mod(y, height) * width) + ImageMath.mod(x, width)];
142                                case ConvolveFilter.CLAMP_EDGES:
143                                        return pixels[(ImageMath.clamp(y, 0, height-1) * width) + ImageMath.clamp(x, 0, width-1)];
144                                }
145                        }
146                        return pixels[ y*width+x ];
147                }
148        }
149        
150    public BufferedImage filter( BufferedImage src, BufferedImage dst ) {
151        int width = src.getWidth();
152        int height = src.getHeight();
153                this.width = src.getWidth();
154                this.height = src.getHeight();
155                //int type = src.getType();
156
157                originalSpace = new Rectangle(0, 0, width, height);
158                transformedSpace = new Rectangle(0, 0, width, height);
159                transformSpace(transformedSpace);
160
161        if ( dst == null ) {
162            ColorModel dstCM = src.getColorModel();
163                        dst = new BufferedImage(dstCM, dstCM.createCompatibleWritableRaster(transformedSpace.width, transformedSpace.height), dstCM.isAlphaPremultiplied(), null);
164                }
165                //WritableRaster dstRaster = 
166        dst.getRaster();
167
168                int[] inPixels = getRGB( src, 0, 0, width, height, null );
169
170                if ( interpolation == NEAREST_NEIGHBOUR )
171                        return filterPixelsNN( dst, width, height, inPixels, transformedSpace );
172
173                int srcWidth = width;
174                int srcHeight = height;
175                int srcWidth1 = width-1;
176                int srcHeight1 = height-1;
177                int outWidth = transformedSpace.width;
178                int outHeight = transformedSpace.height;
179                int outX, outY;
180                //int index = 0;
181                int[] outPixels = new int[outWidth];
182
183                outX = transformedSpace.x;
184                outY = transformedSpace.y;
185                float[] out = new float[4];
186
187                for (int y = 0; y < outHeight; y++) {
188                        for (int x = 0; x < outWidth; x++) {
189                                transformInverse(outX+x, outY+y, out);
190                                int srcX = (int)Math.floor( out[0] );
191                                int srcY = (int)Math.floor( out[1] );
192                                float xWeight = out[0]-srcX;
193                                float yWeight = out[1]-srcY;
194                                int nw, ne, sw, se;
195
196                                if ( srcX >= 0 && srcX < srcWidth1 && srcY >= 0 && srcY < srcHeight1) {
197                                        // Easy case, all corners are in the image
198                                        int i = srcWidth*srcY + srcX;
199                                        nw = inPixels[i];
200                                        ne = inPixels[i+1];
201                                        sw = inPixels[i+srcWidth];
202                                        se = inPixels[i+srcWidth+1];
203                                } else {
204                                        // Some of the corners are off the image
205                                        nw = getPixel( inPixels, srcX, srcY, srcWidth, srcHeight );
206                                        ne = getPixel( inPixels, srcX+1, srcY, srcWidth, srcHeight );
207                                        sw = getPixel( inPixels, srcX, srcY+1, srcWidth, srcHeight );
208                                        se = getPixel( inPixels, srcX+1, srcY+1, srcWidth, srcHeight );
209                                }
210                                int rgb = ImageMath.bilinearInterpolate(xWeight, yWeight, nw, ne, sw, se);
211                                int r = (rgb >> 16) & 0xff;
212                                int g = (rgb >> 8) & 0xff;
213                                int b = rgb & 0xff;
214                                float shade = out[2];
215                                r = (int)(r * shade);
216                                g = (int)(g * shade);
217                                b = (int)(b * shade);
218                                rgb = (rgb & 0xff000000) | (r << 16) | (g << 8) | b;
219                                if ( out[3] != 0 )
220                                        outPixels[x] = PixelUtils.combinePixels( rgb, inPixels[srcWidth*y + x], PixelUtils.NORMAL );
221                                else
222                                        outPixels[x] = rgb;
223                        }
224                        setRGB( dst, 0, y, transformedSpace.width, 1, outPixels );
225                }
226                return dst;
227        }
228
229        final private int getPixel( int[] pixels, int x, int y, int width, int height ) {
230                if (x < 0 || x >= width || y < 0 || y >= height) {
231                        switch (edgeAction) {
232                        case ConvolveFilter.ZERO_EDGES:
233                        default:
234                                return 0;
235                        case ConvolveFilter.WRAP_EDGES:
236                                return pixels[(ImageMath.mod(y, height) * width) + ImageMath.mod(x, width)];
237                        case ConvolveFilter.CLAMP_EDGES:
238                                return pixels[(ImageMath.clamp(y, 0, height-1) * width) + ImageMath.clamp(x, 0, width-1)];
239                        }
240                }
241                return pixels[ y*width+x ];
242        }
243
244        protected void transformInverse(int x, int y, float[] out) {
245/*Fisheye
246                float mirrorDistance = width*centreX;
247                float mirrorRadius = width*centreY;
248                float cx = width*.5f;
249                float cy = height*.5f;
250                float dx = x-cx;
251                float dy = y-cy;
252                float r2 = dx*dx+dy*dy;
253                float r = (float)Math.sqrt( r2 );
254                float phi = (float)(Math.PI*.5-Math.asin( Math.sqrt( mirrorRadius*mirrorRadius-r2 )/mirrorRadius ));
255                r = r > mirrorRadius ? width : mirrorDistance * (float)Math.tan( phi );
256                phi = (float)Math.atan2( dy, dx );
257                out[0] = cx + r*(float)Math.cos( phi );
258                out[1] = cy + r*(float)Math.sin( phi );
259*/
260                float t = transition;
261                float px = x, py = y;
262                float s = (float)Math.sin( angle );
263                float c = (float)Math.cos( angle );
264                float tx = t*width;
265                tx = t * (float)Math.sqrt( width*width + height*height);
266
267                // Start from the correct corner according to the angle
268                float xoffset = c < 0 ? width : 0;
269                float yoffset = s < 0 ? height : 0;
270
271                // Transform into unrotated coordinates
272                px -= xoffset;
273                py -= yoffset;
274
275                float qx = px * c + py * s;
276                float qy = -px * s + py * c;
277
278                boolean outside = qx < tx;
279                boolean unfolded = qx > tx*2;
280                boolean oncurl = !(outside || unfolded);
281
282                qx = qx > tx*2 ? qx : 2*tx-qx;
283        
284                // Transform back into rotated coordinates
285                px = qx * c - qy * s;
286                py = qx * s + qy * c;
287                px += xoffset;
288                py += yoffset;
289
290                // See if we're off the edge of the page
291                boolean offpage = px < 0 || py < 0 || px >= width || py >= height;
292
293                // If we're off the edge, but in the curl...
294                if ( offpage && oncurl ) {
295                        px = x;
296                        py = y;
297                }
298        
299                // Shade the curl
300                float shade = !offpage && oncurl ? 1.9f * (1.0f-(float)Math.cos( Math.exp((qx-tx)/radius) )) : 0;
301                out[2] = 1-shade;
302
303                if ( outside ) {
304                        out[0] = out[1] = -1;
305                } else {
306                        out[0] = px;
307                        out[1] = py;
308                }
309                
310                out[3] = !offpage && oncurl ? 1 : 0;
311        }
312
313        public String toString() {
314                return "Distort/Curl...";
315        }
316
317        public BufferedImage filter(BufferedImage src, Struct parameters) throws PageException {BufferedImage dst=ImageUtil.createBufferedImage(src);
318                Object o;
319                if((o=parameters.removeEL(KeyImpl.init("Radius")))!=null)setRadius(ImageFilterUtil.toFloatValue(o,"Radius"));
320                if((o=parameters.removeEL(KeyImpl.init("Angle")))!=null)setAngle(ImageFilterUtil.toFloatValue(o,"Angle"));
321                if((o=parameters.removeEL(KeyImpl.init("Transition")))!=null)setTransition(ImageFilterUtil.toFloatValue(o,"Transition"));
322                if((o=parameters.removeEL(KeyImpl.init("EdgeAction")))!=null)setEdgeAction(ImageFilterUtil.toString(o,"EdgeAction"));
323                if((o=parameters.removeEL(KeyImpl.init("Interpolation")))!=null)setInterpolation(ImageFilterUtil.toString(o,"Interpolation"));
324
325                // check for arguments not supported
326                if(parameters.size()>0) {
327                        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, Angle, Transition, EdgeAction, Interpolation]");
328                }
329
330                return filter(src, dst);
331        }
332}