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}