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.util.Random;
038
039import lucee.runtime.engine.ThreadLocalPageContext;
040import lucee.runtime.exp.ExpressionException;
041import lucee.runtime.exp.FunctionException;
042import lucee.runtime.exp.PageException;
043import lucee.runtime.img.ImageUtil;
044import lucee.runtime.img.math.Function2D;
045import lucee.runtime.img.math.Noise;
046import lucee.runtime.type.KeyImpl;
047import lucee.runtime.type.Struct;
048import lucee.runtime.type.util.CollectionUtil;
049/**
050 * A filter which produces an image with a cellular texture.
051 */
052public class CellularFilter extends WholeImageFilter implements Function2D, Cloneable, DynFiltering {
053
054        protected float scale = 32;
055        protected float stretch = 1.0f;
056        protected float angle = 0.0f;
057        public float amount = 1.0f;
058        public float turbulence = 1.0f;
059        public float gain = 0.5f;
060        public float bias = 0.5f;
061        public float distancePower = 2;
062        public boolean useColor = false;
063        protected Colormap colormap = new Gradient();
064        protected float[] coefficients = { 1, 0, 0, 0 };
065        protected float angleCoefficient;
066        protected Random random = new Random();
067        protected float m00 = 1.0f;
068        protected float m01 = 0.0f;
069        protected float m10 = 0.0f;
070        protected float m11 = 1.0f;
071        protected Point[] results = null;
072        protected float randomness = 0;
073        protected int gridType = HEXAGONAL;
074        //private float min;
075        //private float max;
076        private static byte[] probabilities;
077        private float gradientCoefficient;
078        
079        public final static int RANDOM = 0;
080        public final static int SQUARE = 1;
081        public final static int HEXAGONAL = 2;
082        public final static int OCTAGONAL = 3;
083        public final static int TRIANGULAR = 4;
084
085        public CellularFilter() {
086                results = new Point[3];
087                for (int j = 0; j < results.length; j++)
088                        results[j] = new Point();
089                if (probabilities == null) {
090                        probabilities = new byte[8192];
091                        float factorial = 1;
092                        float total = 0;
093                        float mean = 2.5f;
094                        for (int i = 0; i < 10; i++) {
095                                if (i > 1)
096                                        factorial *= i;
097                                float probability = (float)Math.pow(mean, i) * (float)Math.exp(-mean) / factorial;
098                                int start = (int)(total * 8192);
099                                total += probability;
100                                int end = (int)(total * 8192);
101                                for (int j = start; j < end; j++)
102                                        probabilities[j] = (byte)i;
103                        }       
104                }
105        }
106        
107        /**
108     * Specifies the scale of the texture.
109     * @param scale the scale of the texture.
110     * @min-value 1
111     * @max-value 300+
112     * @see #getScale
113     */
114        public void setScale(float scale) {
115                this.scale = scale;
116        }
117
118        /**
119     * Returns the scale of the texture.
120     * @return the scale of the texture.
121     * @see #setScale
122     */
123        public float getScale() {
124                return scale;
125        }
126
127        /**
128     * Specifies the stretch factor of the texture.
129     * @param stretch the stretch factor of the texture.
130     * @min-value 1
131     * @max-value 50+
132     * @see #getStretch
133     */
134        public void setStretch(float stretch) {
135                this.stretch = stretch;
136        }
137
138        /**
139     * Returns the stretch factor of the texture.
140     * @return the stretch factor of the texture.
141     * @see #setStretch
142     */
143        public float getStretch() {
144                return stretch;
145        }
146
147        /**
148     * Specifies the angle of the texture.
149     * @param angle the angle of the texture.
150     * @angle
151     * @see #getAngle
152     */
153        public void setAngle(float angle) {
154                this.angle = angle;
155                float cos = (float)Math.cos(angle);
156                float sin = (float)Math.sin(angle);
157                m00 = cos;
158                m01 = sin;
159                m10 = -sin;
160                m11 = cos;
161        }
162
163        /**
164     * Returns the angle of the texture.
165     * @return the angle of the texture.
166     * @see #setAngle
167     */
168        public float getAngle() {
169                return angle;
170        }
171
172        public void setCoefficient(int i, float v) {
173                coefficients[i] = v;
174        }
175
176        public float getCoefficient(int i) {
177                return coefficients[i];
178        }
179
180        public void setAngleCoefficient(float angleCoefficient) {
181                this.angleCoefficient = angleCoefficient;
182        }
183
184        public float getAngleCoefficient() {
185                return angleCoefficient;
186        }
187
188        public void setGradientCoefficient(float gradientCoefficient) {
189                this.gradientCoefficient = gradientCoefficient;
190        }
191
192        public float getGradientCoefficient() {
193                return gradientCoefficient;
194        }
195
196        public void setF1( float v ) {
197                coefficients[0] = v;
198        }
199
200        public float getF1() {
201                return coefficients[0];
202        }
203
204        public void setF2( float v ) {
205                coefficients[1] = v;
206        }
207
208        public float getF2() {
209                return coefficients[1];
210        }
211
212        public void setF3( float v ) {
213                coefficients[2] = v;
214        }
215
216        public float getF3() {
217                return coefficients[2];
218        }
219
220        public void setF4( float v ) {
221                coefficients[3] = v;
222        }
223
224        public float getF4() {
225                return coefficients[3];
226        }
227
228    /**
229     * Set the colormap to be used for the filter.
230     * @param colormap the colormap
231     * @see #getColormap
232     */
233        public void setColormap(Colormap colormap) {
234                this.colormap = colormap;
235        }
236        
237    /**
238     * Get the colormap to be used for the filter.
239     * @return the colormap
240     * @see #setColormap
241     */
242        public Colormap getColormap() {
243                return colormap;
244        }
245        
246        public void setRandomness(float randomness) {
247                this.randomness = randomness;
248        }
249
250        public float getRandomness() {
251                return randomness;
252        }
253
254        /**
255         * the grid type to set, one of the following:
256         * -  RANDOM
257         * -  SQUARE
258         * -  HEXAGONAL
259         * -  OCTAGONAL
260         * -  TRIANGULAR
261         */
262        public void setGridType(String gridType) throws ExpressionException {
263                gridType=gridType.trim().toLowerCase();
264                if("random".equals(gridType)) this.gridType = RANDOM;
265                else if("square".equals(gridType)) this.gridType = SQUARE;
266                else if("hexagonal".equals(gridType)) this.gridType = HEXAGONAL;
267                else if("octagonal".equals(gridType)) this.gridType = OCTAGONAL;
268                else if("triangular".equals(gridType)) this.gridType = TRIANGULAR;
269                else 
270                        throw new ExpressionException("invalid value ["+gridType+"] for gridType, valid values are [random,square,hexagonal,octagonal,triangular]");
271        }
272
273        public int getGridType() {
274                return gridType;
275        }
276
277        public void setDistancePower(float distancePower) {
278                this.distancePower = distancePower;
279        }
280
281        public float getDistancePower() {
282                return distancePower;
283        }
284
285        /**
286     * Specifies the turbulence of the texture.
287     * @param turbulence the turbulence of the texture.
288     * @min-value 0
289     * @max-value 1
290     * @see #getTurbulence
291     */
292        public void setTurbulence(float turbulence) {
293                this.turbulence = turbulence;
294        }
295
296        /**
297     * Returns the turbulence of the effect.
298     * @return the turbulence of the effect.
299     * @see #setTurbulence
300     */
301        public float getTurbulence() {
302                return turbulence;
303        }
304
305        /**
306         * Set the amount of effect.
307         * @param amount the amount
308     * @min-value 0
309     * @max-value 1
310     * @see #getAmount
311         */
312        public void setAmount(float amount) {
313                this.amount = amount;
314        }
315
316        /**
317         * Get the amount of texture.
318         * @return the amount
319     * @see #setAmount
320         */
321        public float getAmount() {
322                return amount;
323        }
324
325        public class Point {
326                public int index;
327                public float x, y;
328                public float dx, dy;
329                public float cubeX, cubeY;
330                public float distance;
331        }
332        
333        private float checkCube(float x, float y, int cubeX, int cubeY, Point[] results) {
334                int numPoints;
335                random.setSeed(571*cubeX + 23*cubeY);
336                switch (gridType) {
337                case RANDOM:
338                default:
339                        numPoints = probabilities[random.nextInt() & 0x1fff];
340                        break;
341                case SQUARE:
342                        numPoints = 1;
343                        break;
344                case HEXAGONAL:
345                        numPoints = 1;
346                        break;
347                case OCTAGONAL:
348                        numPoints = 2;
349                        break;
350                case TRIANGULAR:
351                        numPoints = 2;
352                        break;
353                }
354                for (int i = 0; i < numPoints; i++) {
355                        float px = 0, py = 0;
356                        float weight = 1.0f;
357                        switch (gridType) {
358                        case RANDOM:
359                                px = random.nextFloat();
360                                py = random.nextFloat();
361                                break;
362                        case SQUARE:
363                                px = py = 0.5f;
364                                if (randomness != 0) {
365                                        px += randomness * (random.nextFloat()-0.5);
366                                        py += randomness * (random.nextFloat()-0.5);
367                                }
368                                break;
369                        case HEXAGONAL:
370                                if ((cubeX & 1) == 0) {
371                                        px = 0.75f; py = 0;
372                                } else {
373                                        px = 0.75f; py = 0.5f;
374                                }
375                                if (randomness != 0) {
376                                        px += randomness * Noise.noise2(271*(cubeX+px), 271*(cubeY+py));
377                                        py += randomness * Noise.noise2(271*(cubeX+px)+89, 271*(cubeY+py)+137);
378                                }
379                                break;
380                        case OCTAGONAL:
381                                switch (i) {
382                                case 0: px = 0.207f; py = 0.207f; break;
383                                case 1: px = 0.707f; py = 0.707f; weight = 1.6f; break;
384                                }
385                                if (randomness != 0) {
386                                        px += randomness * Noise.noise2(271*(cubeX+px), 271*(cubeY+py));
387                                        py += randomness * Noise.noise2(271*(cubeX+px)+89, 271*(cubeY+py)+137);
388                                }
389                                break;
390                        case TRIANGULAR:
391                                if ((cubeY & 1) == 0) {
392                                        if (i == 0) {
393                                                px = 0.25f; py = 0.35f;
394                                        } else {
395                                                px = 0.75f; py = 0.65f;
396                                        }
397                                } else {
398                                        if (i == 0) {
399                                                px = 0.75f; py = 0.35f;
400                                        } else {
401                                                px = 0.25f; py = 0.65f;
402                                        }
403                                }
404                                if (randomness != 0) {
405                                        px += randomness * Noise.noise2(271*(cubeX+px), 271*(cubeY+py));
406                                        py += randomness * Noise.noise2(271*(cubeX+px)+89, 271*(cubeY+py)+137);
407                                }
408                                break;
409                        }
410                        float dx = Math.abs(x-px);
411                        float dy = Math.abs(y-py);
412                        float d;
413                        dx *= weight;
414                        dy *= weight;
415                        if (distancePower == 1.0f)
416                                d = dx + dy;
417                        else if (distancePower == 2.0f)
418                                d = (float)Math.sqrt(dx*dx + dy*dy);
419                        else
420                                d = (float)Math.pow((float)Math.pow(dx, distancePower) + (float)Math.pow(dy, distancePower), 1/distancePower);
421
422                        // Insertion sort the long way round to speed it up a bit
423                        if (d < results[0].distance) {
424                                Point p = results[2];
425                                results[2] = results[1];
426                                results[1] = results[0];
427                                results[0] = p;
428                                p.distance = d;
429                                p.dx = dx;
430                                p.dy = dy;
431                                p.x = cubeX+px;
432                                p.y = cubeY+py;
433                        } else if (d < results[1].distance) {
434                                Point p = results[2];
435                                results[2] = results[1];
436                                results[1] = p;
437                                p.distance = d;
438                                p.dx = dx;
439                                p.dy = dy;
440                                p.x = cubeX+px;
441                                p.y = cubeY+py;
442                        } else if (d < results[2].distance) {
443                                Point p = results[2];
444                                p.distance = d;
445                                p.dx = dx;
446                                p.dy = dy;
447                                p.x = cubeX+px;
448                                p.y = cubeY+py;
449                        }
450                }
451                return results[2].distance;
452        }
453        
454        public float evaluate(float x, float y) {
455                for (int j = 0; j < results.length; j++)
456                        results[j].distance = Float.POSITIVE_INFINITY;
457
458                int ix = (int)x;
459                int iy = (int)y;
460                float fx = x-ix;
461                float fy = y-iy;
462
463                float d = checkCube(fx, fy, ix, iy, results);
464                if (d > fy)
465                        d = checkCube(fx, fy+1, ix, iy-1, results);
466                if (d > 1-fy)
467                        d = checkCube(fx, fy-1, ix, iy+1, results);
468                if (d > fx) {
469                        checkCube(fx+1, fy, ix-1, iy, results);
470                        if (d > fy)
471                                d = checkCube(fx+1, fy+1, ix-1, iy-1, results);
472                        if (d > 1-fy)
473                                d = checkCube(fx+1, fy-1, ix-1, iy+1, results);
474                }
475                if (d > 1-fx) {
476                        d = checkCube(fx-1, fy, ix+1, iy, results);
477                        if (d > fy)
478                                d = checkCube(fx-1, fy+1, ix+1, iy-1, results);
479                        if (d > 1-fy)
480                                d = checkCube(fx-1, fy-1, ix+1, iy+1, results);
481                }
482
483                float t = 0;
484                for (int i = 0; i < 3; i++)
485                        t += coefficients[i] * results[i].distance;
486                if (angleCoefficient != 0) {
487                        float angle = (float)Math.atan2(y-results[0].y, x-results[0].x);
488                        if (angle < 0)
489                                angle += 2*(float)Math.PI;
490                        angle /= 4*(float)Math.PI;
491                        t += angleCoefficient * angle;
492                }
493                if (gradientCoefficient != 0) {
494                        float a = 1/(results[0].dy+results[0].dx);
495                        t += gradientCoefficient * a;
496                }
497                return t;
498        }
499        
500        public float turbulence2(float x, float y, float freq) {
501                float t = 0.0f;
502
503                for (float f = 1.0f; f <= freq; f *= 2)
504                        t += evaluate(f*x, f*y) / f;
505                return t;
506        }
507
508        public int getPixel(int x, int y, int[] inPixels, int width, int height) {
509                float nx = m00*x + m01*y;
510                float ny = m10*x + m11*y;
511                nx /= scale;
512                ny /= scale * stretch;
513                nx += 1000;
514                ny += 1000;     // Reduce artifacts around 0,0
515                float f = turbulence == 1.0f ? evaluate(nx, ny) : turbulence2(nx, ny, turbulence);
516                // Normalize to 0..1
517//              f = (f-min)/(max-min);
518                f *= 2;
519                f *= amount;
520                int a = 0xff000000;
521                int v;
522                if (colormap != null) {
523                        v = colormap.getColor(f);
524                        if (useColor) {
525                                int srcx = ImageMath.clamp((int)((results[0].x-1000)*scale), 0, width-1);
526                                int srcy = ImageMath.clamp((int)((results[0].y-1000)*scale), 0, height-1);
527                                v = inPixels[srcy * width + srcx];
528                                f = (results[1].distance - results[0].distance) / (results[1].distance + results[0].distance);
529                                f = ImageMath.smoothStep(coefficients[1], coefficients[0], f);
530                                v = ImageMath.mixColors(f, 0xff000000, v);
531                        }
532                        return v;
533                }
534                v = PixelUtils.clamp((int)(f*255));
535                int r = v << 16;
536                int g = v << 8;
537                int b = v;
538                return a|r|g|b;
539        }
540
541        protected int[] filterPixels( int width, int height, int[] inPixels, Rectangle transformedSpace ) {
542                int index = 0;
543                int[] outPixels = new int[width * height];
544
545                for (int y = 0; y < height; y++) {
546                        for (int x = 0; x < width; x++) {
547                                outPixels[index++] = getPixel(x, y, inPixels, width, height);
548                        }
549                }
550                return outPixels;
551        }
552
553        public Object clone() {
554                CellularFilter f = (CellularFilter)super.clone();
555                f.coefficients = coefficients.clone();
556                f.results = results.clone();
557                f.random = new Random();
558//              if (colormap != null)
559//                      f.colormap = (Colormap)colormap.clone();
560                return f;
561        }
562        
563        public String toString() {
564                return "Texture/Cellular...";
565        }
566        
567        public BufferedImage filter(BufferedImage src, Struct parameters) throws PageException {BufferedImage dst=ImageUtil.createBufferedImage(src);
568                Object o;
569                if((o=parameters.removeEL(KeyImpl.init("Colormap")))!=null)setColormap(ImageFilterUtil.toColormap(o,"Colormap"));
570                if((o=parameters.removeEL(KeyImpl.init("Amount")))!=null)setAmount(ImageFilterUtil.toFloatValue(o,"Amount"));
571                if((o=parameters.removeEL(KeyImpl.init("Turbulence")))!=null)setTurbulence(ImageFilterUtil.toFloatValue(o,"Turbulence"));
572                if((o=parameters.removeEL(KeyImpl.init("Stretch")))!=null)setStretch(ImageFilterUtil.toFloatValue(o,"Stretch"));
573                if((o=parameters.removeEL(KeyImpl.init("Angle")))!=null)setAngle(ImageFilterUtil.toFloatValue(o,"Angle"));
574                if((o=parameters.removeEL(KeyImpl.init("AngleCoefficient")))!=null)setAngleCoefficient(ImageFilterUtil.toFloatValue(o,"AngleCoefficient"));
575                if((o=parameters.removeEL(KeyImpl.init("GradientCoefficient")))!=null)setGradientCoefficient(ImageFilterUtil.toFloatValue(o,"GradientCoefficient"));
576                if((o=parameters.removeEL(KeyImpl.init("F1")))!=null)setF1(ImageFilterUtil.toFloatValue(o,"F1"));
577                if((o=parameters.removeEL(KeyImpl.init("F2")))!=null)setF2(ImageFilterUtil.toFloatValue(o,"F2"));
578                if((o=parameters.removeEL(KeyImpl.init("F3")))!=null)setF3(ImageFilterUtil.toFloatValue(o,"F3"));
579                if((o=parameters.removeEL(KeyImpl.init("F4")))!=null)setF4(ImageFilterUtil.toFloatValue(o,"F4"));
580                if((o=parameters.removeEL(KeyImpl.init("Randomness")))!=null)setRandomness(ImageFilterUtil.toFloatValue(o,"Randomness"));
581                if((o=parameters.removeEL(KeyImpl.init("GridType")))!=null)setGridType(ImageFilterUtil.toString(o,"GridType"));
582                if((o=parameters.removeEL(KeyImpl.init("DistancePower")))!=null)setDistancePower(ImageFilterUtil.toFloatValue(o,"DistancePower"));
583                if((o=parameters.removeEL(KeyImpl.init("Scale")))!=null)setScale(ImageFilterUtil.toFloatValue(o,"Scale"));
584
585                // check for arguments not supported
586                if(parameters.size()>0) {
587                        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 [Colormap, Amount, Turbulence, Stretch, Angle, Coefficient, AngleCoefficient, GradientCoefficient, F1, F2, F3, F4, Randomness, GridType, DistancePower, Scale]");
588                }
589
590                return filter(src, dst);
591        }
592}