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.image.BufferedImage; 036 037import lucee.runtime.engine.ThreadLocalPageContext; 038import lucee.runtime.exp.FunctionException; 039import lucee.runtime.exp.PageException; 040import lucee.runtime.img.ImageUtil; 041import lucee.runtime.img.math.FFT; 042import lucee.runtime.type.KeyImpl; 043import lucee.runtime.type.Struct; 044import lucee.runtime.type.util.CollectionUtil; 045 046/** 047 * A filter which use FFTs to simulate lens blur on an image. 048 */ 049public class LensBlurFilter extends AbstractBufferedImageOp implements DynFiltering { 050 051 private float radius = 10; 052 private float bloom = 2; 053 private float bloomThreshold = 192; 054 private float angle = 0; 055 private int sides = 5; 056 057 /** 058 * Set the radius of the kernel, and hence the amount of blur. 059 * @param radius the radius of the blur in pixels. 060 * @see #getRadius 061 */ 062 public void setRadius(float radius) { 063 this.radius = radius; 064 } 065 066 /** 067 * Get the radius of the kernel. 068 * @return the radius 069 * @see #setRadius 070 */ 071 public float getRadius() { 072 return radius; 073 } 074 075 /** 076 * Set the number of sides of the aperture. 077 * @param sides the number of sides 078 * @see #getSides 079 */ 080 public void setSides(int sides) { 081 this.sides = sides; 082 } 083 084 /** 085 * Get the number of sides of the aperture. 086 * @return the number of sides 087 * @see #setSides 088 */ 089 public int getSides() { 090 return sides; 091 } 092 093 /** 094 * Set the bloom factor. 095 * @param bloom the bloom factor 096 * @see #getBloom 097 */ 098 public void setBloom(float bloom) { 099 this.bloom = bloom; 100 } 101 102 /** 103 * Get the bloom factor. 104 * @return the bloom factor 105 * @see #setBloom 106 */ 107 public float getBloom() { 108 return bloom; 109 } 110 111 /** 112 * Set the bloom threshold. 113 * @param bloomThreshold the bloom threshold 114 * @see #getBloomThreshold 115 */ 116 public void setBloomThreshold(float bloomThreshold) { 117 this.bloomThreshold = bloomThreshold; 118 } 119 120 /** 121 * Get the bloom threshold. 122 * @return the bloom threshold 123 * @see #setBloomThreshold 124 */ 125 public float getBloomThreshold() { 126 return bloomThreshold; 127 } 128 129 130 public BufferedImage filter( BufferedImage src, BufferedImage dst ) { 131 int width = src.getWidth(); 132 int height = src.getHeight(); 133 int rows = 1, cols = 1; 134 int log2rows = 0, log2cols = 0; 135 int iradius = (int)Math.ceil(radius); 136 int tileWidth = 128; 137 int tileHeight = tileWidth; 138 139 //int adjustedWidth =(width + iradius*2); 140 //int adjustedHeight =(height + iradius*2); 141 142 tileWidth = iradius < 32 ? Math.min(128, width+2*iradius) : Math.min(256, width+2*iradius); 143 tileHeight = iradius < 32 ? Math.min(128, height+2*iradius) : Math.min(256, height+2*iradius); 144 145 if ( dst == null ) 146 dst = new BufferedImage( width, height, BufferedImage.TYPE_INT_ARGB ); 147 148 while (rows < tileHeight) { 149 rows *= 2; 150 log2rows++; 151 } 152 while (cols < tileWidth) { 153 cols *= 2; 154 log2cols++; 155 } 156 int w = cols; 157 int h = rows; 158 159 tileWidth = w; 160 tileHeight = h;//FIXME-tileWidth, w, and cols are always all the same 161 162 FFT fft = new FFT( Math.max(log2rows, log2cols) ); 163 164 int[] rgb = new int[w*h]; 165 float[][] mask = new float[2][w*h]; 166 float[][] gb = new float[2][w*h]; 167 float[][] ar = new float[2][w*h]; 168 169 // Create the kernel 170 double polyAngle = Math.PI/sides; 171 double polyScale = 1.0f / Math.cos(polyAngle); 172 double r2 = radius*radius; 173 double rangle = Math.toRadians(angle); 174 float total = 0; 175 int i = 0; 176 for ( int y = 0; y < h; y++ ) { 177 for ( int x = 0; x < w; x++ ) { 178 double dx = x-w/2f; 179 double dy = y-h/2f; 180 double r = dx*dx+dy*dy; 181 double f = r < r2 ? 1 : 0; 182 if (f != 0) { 183 r = Math.sqrt(r); 184 if ( sides != 0 ) { 185 double a = Math.atan2(dy, dx)+rangle; 186 a = ImageMath.mod(a, polyAngle*2)-polyAngle; 187 f = Math.cos(a) * polyScale; 188 } else 189 f = 1; 190 f = f*r < radius ? 1 : 0; 191 } 192 total += (float)f; 193 194 mask[0][i] = (float)f; 195 mask[1][i] = 0; 196 i++; 197 } 198 } 199 200 // Normalize the kernel 201 i = 0; 202 for ( int y = 0; y < h; y++ ) { 203 for ( int x = 0; x < w; x++ ) { 204 mask[0][i] /= total; 205 i++; 206 } 207 } 208 209 fft.transform2D( mask[0], mask[1], w, h, true ); 210 211 for ( int tileY = -iradius; tileY < height; tileY += tileHeight-2*iradius ) { 212 for ( int tileX = -iradius; tileX < width; tileX += tileWidth-2*iradius ) { 213// System.out.println("Tile: "+tileX+" "+tileY+" "+tileWidth+" "+tileHeight); 214 215 // Clip the tile to the image bounds 216 int tx = tileX, ty = tileY, tw = tileWidth, th = tileHeight; 217 int fx = 0, fy = 0; 218 if ( tx < 0 ) { 219 tw += tx; 220 fx -= tx; 221 tx = 0; 222 } 223 if ( ty < 0 ) { 224 th += ty; 225 fy -= ty; 226 ty = 0; 227 } 228 if ( tx+tw > width ) 229 tw = width-tx; 230 if ( ty+th > height ) 231 th = height-ty; 232 src.getRGB( tx, ty, tw, th, rgb, fy*w+fx, w ); 233 234 // Create a float array from the pixels. Any pixels off the edge of the source image get duplicated from the edge. 235 i = 0; 236 for ( int y = 0; y < h; y++ ) { 237 int imageY = y+tileY; 238 int j; 239 if ( imageY < 0 ) 240 j = fy; 241 else if ( imageY > height ) 242 j = fy+th-1; 243 else 244 j = y; 245 j *= w; 246 for ( int x = 0; x < w; x++ ) { 247 int imageX = x+tileX; 248 int k; 249 if ( imageX < 0 ) 250 k = fx; 251 else if ( imageX > width ) 252 k = fx+tw-1; 253 else 254 k = x; 255 k += j; 256 257 ar[0][i] = ((rgb[k] >> 24) & 0xff); 258 float r = ((rgb[k] >> 16) & 0xff); 259 float g = ((rgb[k] >> 8) & 0xff); 260 float b = (rgb[k] & 0xff); 261 262 // Bloom... 263 if ( r > bloomThreshold ) 264 r *= bloom; 265// r = bloomThreshold + (r-bloomThreshold) * bloom; 266 if ( g > bloomThreshold ) 267 g *= bloom; 268// g = bloomThreshold + (g-bloomThreshold) * bloom; 269 if ( b > bloomThreshold ) 270 b *= bloom; 271// b = bloomThreshold + (b-bloomThreshold) * bloom; 272 273 ar[1][i] = r; 274 gb[0][i] = g; 275 gb[1][i] = b; 276 277 i++; 278 k++; 279 } 280 } 281 282 // Transform into frequency space 283 fft.transform2D( ar[0], ar[1], cols, rows, true ); 284 fft.transform2D( gb[0], gb[1], cols, rows, true ); 285 286 // Multiply the transformed pixels by the transformed kernel 287 i = 0; 288 for ( int y = 0; y < h; y++ ) { 289 for ( int x = 0; x < w; x++ ) { 290 float re = ar[0][i]; 291 float im = ar[1][i]; 292 float rem = mask[0][i]; 293 float imm = mask[1][i]; 294 ar[0][i] = re*rem-im*imm; 295 ar[1][i] = re*imm+im*rem; 296 297 re = gb[0][i]; 298 im = gb[1][i]; 299 gb[0][i] = re*rem-im*imm; 300 gb[1][i] = re*imm+im*rem; 301 i++; 302 } 303 } 304 305 // Transform back 306 fft.transform2D( ar[0], ar[1], cols, rows, false ); 307 fft.transform2D( gb[0], gb[1], cols, rows, false ); 308 309 // Convert back to RGB pixels, with quadrant remapping 310 int row_flip = w >> 1; 311 int col_flip = h >> 1; 312 int index = 0; 313 314 //FIXME-don't bother converting pixels off image edges 315 for ( int y = 0; y < w; y++ ) { 316 int ym = y ^ row_flip; 317 int yi = ym*cols; 318 for ( int x = 0; x < w; x++ ) { 319 int xm = yi + (x ^ col_flip); 320 int a = (int)ar[0][xm]; 321 int r = (int)ar[1][xm]; 322 int g = (int)gb[0][xm]; 323 int b = (int)gb[1][xm]; 324 325 // Clamp high pixels due to blooming 326 if ( r > 255 ) 327 r = 255; 328 if ( g > 255 ) 329 g = 255; 330 if ( b > 255 ) 331 b = 255; 332 int argb = (a << 24) | (r << 16) | (g << 8) | b; 333 rgb[index++] = argb; 334 } 335 } 336 337 // Clip to the output image 338 tx = tileX+iradius; 339 ty = tileY+iradius; 340 tw = tileWidth-2*iradius; 341 th = tileHeight-2*iradius; 342 if ( tx+tw > width ) 343 tw = width-tx; 344 if ( ty+th > height ) 345 th = height-ty; 346 dst.setRGB( tx, ty, tw, th, rgb, iradius*w+iradius, w ); 347 } 348 } 349 return dst; 350 } 351 352 public String toString() { 353 return "Blur/Lens Blur..."; 354 } 355 public BufferedImage filter(BufferedImage src, Struct parameters) throws PageException {BufferedImage dst=ImageUtil.createBufferedImage(src); 356 Object o; 357 if((o=parameters.removeEL(KeyImpl.init("Radius")))!=null)setRadius(ImageFilterUtil.toFloatValue(o,"Radius")); 358 if((o=parameters.removeEL(KeyImpl.init("Sides")))!=null)setSides(ImageFilterUtil.toIntValue(o,"Sides")); 359 if((o=parameters.removeEL(KeyImpl.init("Bloom")))!=null)setBloom(ImageFilterUtil.toFloatValue(o,"Bloom")); 360 if((o=parameters.removeEL(KeyImpl.init("BloomThreshold")))!=null)setBloomThreshold(ImageFilterUtil.toFloatValue(o,"BloomThreshold")); 361 362 // check for arguments not supported 363 if(parameters.size()>0) { 364 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, Sides, Bloom, BloomThreshold]"); 365 } 366 367 return filter(src, dst); 368 } 369}