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.RenderingHints; 037import java.awt.geom.Point2D; 038import java.awt.geom.Rectangle2D; 039import java.awt.image.BufferedImage; 040import java.awt.image.ColorModel; 041import java.awt.image.Kernel; 042 043import lucee.runtime.engine.ThreadLocalPageContext; 044import lucee.runtime.exp.ExpressionException; 045import lucee.runtime.exp.FunctionException; 046import lucee.runtime.exp.PageException; 047import lucee.runtime.img.ImageUtil; 048import lucee.runtime.type.KeyImpl; 049import lucee.runtime.type.Struct; 050import lucee.runtime.type.util.CollectionUtil; 051 052public abstract class ConvolveFilter extends AbstractBufferedImageOp implements DynFiltering { 053 054 /** 055 * Treat pixels off the edge as zero. 056 */ 057 public final static int ZERO_EDGES = 0; 058 059 /** 060 * Clamp pixels off the edge to the nearest edge. 061 */ 062 public final static int CLAMP_EDGES = 1; 063 064 /** 065 * Wrap pixels off the edge to the opposite edge. 066 */ 067 public final static int WRAP_EDGES = 2; 068 069 /** 070 * The convolution kernel. 071 */ 072 protected Kernel kernel = null; 073 074 /** 075 * Whether to convolve alpha. 076 */ 077 protected boolean alpha = true; 078 079 /** 080 * Whether to promultiply the alpha before convolving. 081 */ 082 protected boolean premultiplyAlpha = true; 083 084 /** 085 * What do do at the image edges. 086 */ 087 private int edgeAction = CLAMP_EDGES; 088 089 /** 090 * Construct a filter with a null kernel. This is only useful if you're going to change the kernel later on. 091 */ 092 public ConvolveFilter() { 093 this(new float[9]); 094 } 095 096 /** 097 * Construct a filter with the given 3x3 kernel. 098 * @param matrix an array of 9 floats containing the kernel 099 */ 100 public ConvolveFilter(float[] matrix) { 101 this(new Kernel(3, 3, matrix)); 102 } 103 104 /** 105 * Construct a filter with the given kernel. 106 * @param rows the number of rows in the kernel 107 * @param cols the number of columns in the kernel 108 * @param matrix an array of rows*cols floats containing the kernel 109 */ 110 public ConvolveFilter(int rows, int cols, float[] matrix) { 111 this(new Kernel(cols, rows, matrix)); 112 } 113 114 /** 115 * Construct a filter with the given 3x3 kernel. 116 * @param kernel the convolution kernel 117 */ 118 public ConvolveFilter(Kernel kernel) { 119 this.kernel = kernel; 120 } 121 122 /** 123 * Set the convolution kernel. 124 * @param kernel the kernel 125 * @see #getKernel 126 */ 127 public void setKernel(Kernel kernel) { 128 this.kernel = kernel; 129 } 130 131 /** 132 * Get the convolution kernel. 133 * @return the kernel 134 * @see #setKernel 135 */ 136 public Kernel getKernel() { 137 return kernel; 138 } 139 140 /** 141 * Set the action to perfomr for pixels off the image edges. 142 * valid values are: 143 * - clamp (default): Clamp pixels off the edge to the nearest edge. 144 * - wrap: Wrap pixels off the edge to the opposite edge. 145 * - zero: Treat pixels off the edge as zero 146 * 147 * @param edgeAction the action 148 * @throws ExpressionException 149 */ 150 public void setEdgeAction(String edgeAction) throws ExpressionException { 151 String str=edgeAction.trim().toUpperCase(); 152 if("ZERO".equals(str)) this.edgeAction = ZERO_EDGES; 153 else if("CLAMP".equals(str)) this.edgeAction = CLAMP_EDGES; 154 else if("WRAP".equals(str)) this.edgeAction = WRAP_EDGES; 155 else 156 throw new ExpressionException("invalid value ["+edgeAction+"] for edgeAction, valid values are [clamp,wrap,zero]"); 157 } 158 159 /** 160 * Get the action to perfomr for pixels off the image edges. 161 * @return the action 162 * @see #setEdgeAction 163 */ 164 public int getEdgeAction() { 165 return edgeAction; 166 } 167 168 /** 169 * Set whether to convolve the alpha channel. 170 * @param useAlpha true to convolve the alpha 171 * @see #getUseAlpha 172 */ 173 public void setUseAlpha( boolean useAlpha ) { 174 this.alpha = useAlpha; 175 } 176 177 /** 178 * Get whether to convolve the alpha channel. 179 * @return true to convolve the alpha 180 * @see #setUseAlpha 181 */ 182 public boolean getUseAlpha() { 183 return alpha; 184 } 185 186 /** 187 * Set whether to premultiply the alpha channel. 188 * @param premultiplyAlpha true to premultiply the alpha 189 * @see #getPremultiplyAlpha 190 */ 191 public void setPremultiplyAlpha( boolean premultiplyAlpha ) { 192 this.premultiplyAlpha = premultiplyAlpha; 193 } 194 195 /** 196 * Get whether to premultiply the alpha channel. 197 * @return true to premultiply the alpha 198 * @see #setPremultiplyAlpha 199 */ 200 public boolean getPremultiplyAlpha() { 201 return premultiplyAlpha; 202 } 203 204 public BufferedImage filter( BufferedImage src, BufferedImage dst ) { 205 int width = src.getWidth(); 206 int height = src.getHeight(); 207 208 if ( dst == null ) 209 dst = createCompatibleDestImage( src, null ); 210 211 int[] inPixels = new int[width*height]; 212 int[] outPixels = new int[width*height]; 213 getRGB( src, 0, 0, width, height, inPixels ); 214 215 if ( premultiplyAlpha ) 216 ImageMath.premultiply( inPixels, 0, inPixels.length ); 217 convolve(kernel, inPixels, outPixels, width, height, alpha, edgeAction); 218 if ( premultiplyAlpha ) 219 ImageMath.unpremultiply( outPixels, 0, outPixels.length ); 220 221 setRGB( dst, 0, 0, width, height, outPixels ); 222 return dst; 223 } 224 225 public BufferedImage createCompatibleDestImage(BufferedImage src, ColorModel dstCM) { 226 if ( dstCM == null ) 227 dstCM = src.getColorModel(); 228 return new BufferedImage(dstCM, dstCM.createCompatibleWritableRaster(src.getWidth(), src.getHeight()), dstCM.isAlphaPremultiplied(), null); 229 } 230 231 public Rectangle2D getBounds2D( BufferedImage src ) { 232 return new Rectangle(0, 0, src.getWidth(), src.getHeight()); 233 } 234 235 public Point2D getPoint2D( Point2D srcPt, Point2D dstPt ) { 236 if ( dstPt == null ) 237 dstPt = new Point2D.Double(); 238 dstPt.setLocation( srcPt.getX(), srcPt.getY() ); 239 return dstPt; 240 } 241 242 public RenderingHints getRenderingHints() { 243 return null; 244 } 245 246 /** 247 * Convolve a block of pixels. 248 * @param kernel the kernel 249 * @param inPixels the input pixels 250 * @param outPixels the output pixels 251 * @param width the width 252 * @param height the height 253 * @param edgeAction what to do at the edges 254 */ 255 public static void convolve(Kernel kernel, int[] inPixels, int[] outPixels, int width, int height, int edgeAction) { 256 convolve(kernel, inPixels, outPixels, width, height, true, edgeAction); 257 } 258 259 /** 260 * Convolve a block of pixels. 261 * @param kernel the kernel 262 * @param inPixels the input pixels 263 * @param outPixels the output pixels 264 * @param width the width 265 * @param height the height 266 * @param alpha include alpha channel 267 * @param edgeAction what to do at the edges 268 */ 269 public static void convolve(Kernel kernel, int[] inPixels, int[] outPixels, int width, int height, boolean alpha, int edgeAction) { 270 if (kernel.getHeight() == 1) 271 convolveH(kernel, inPixels, outPixels, width, height, alpha, edgeAction); 272 else if (kernel.getWidth() == 1) 273 convolveV(kernel, inPixels, outPixels, width, height, alpha, edgeAction); 274 else 275 convolveHV(kernel, inPixels, outPixels, width, height, alpha, edgeAction); 276 } 277 278 /** 279 * Convolve with a 2D kernel. 280 * @param kernel the kernel 281 * @param inPixels the input pixels 282 * @param outPixels the output pixels 283 * @param width the width 284 * @param height the height 285 * @param alpha include alpha channel 286 * @param edgeAction what to do at the edges 287 */ 288 public static void convolveHV(Kernel kernel, int[] inPixels, int[] outPixels, int width, int height, boolean alpha, int edgeAction) { 289 int index = 0; 290 float[] matrix = kernel.getKernelData( null ); 291 int rows = kernel.getHeight(); 292 int cols = kernel.getWidth(); 293 int rows2 = rows/2; 294 int cols2 = cols/2; 295 296 for (int y = 0; y < height; y++) { 297 for (int x = 0; x < width; x++) { 298 float r = 0, g = 0, b = 0, a = 0; 299 300 for (int row = -rows2; row <= rows2; row++) { 301 int iy = y+row; 302 int ioffset; 303 if (0 <= iy && iy < height) 304 ioffset = iy*width; 305 else if ( edgeAction == CLAMP_EDGES ) 306 ioffset = y*width; 307 else if ( edgeAction == WRAP_EDGES ) 308 ioffset = ((iy+height) % height) * width; 309 else 310 continue; 311 int moffset = cols*(row+rows2)+cols2; 312 for (int col = -cols2; col <= cols2; col++) { 313 float f = matrix[moffset+col]; 314 315 if (f != 0) { 316 int ix = x+col; 317 if (!(0 <= ix && ix < width)) { 318 if ( edgeAction == CLAMP_EDGES ) 319 ix = x; 320 else if ( edgeAction == WRAP_EDGES ) 321 ix = (x+width) % width; 322 else 323 continue; 324 } 325 int rgb = inPixels[ioffset+ix]; 326 a += f * ((rgb >> 24) & 0xff); 327 r += f * ((rgb >> 16) & 0xff); 328 g += f * ((rgb >> 8) & 0xff); 329 b += f * (rgb & 0xff); 330 } 331 } 332 } 333 int ia = alpha ? PixelUtils.clamp((int)(a+0.5)) : 0xff; 334 int ir = PixelUtils.clamp((int)(r+0.5)); 335 int ig = PixelUtils.clamp((int)(g+0.5)); 336 int ib = PixelUtils.clamp((int)(b+0.5)); 337 outPixels[index++] = (ia << 24) | (ir << 16) | (ig << 8) | ib; 338 } 339 } 340 } 341 342 /** 343 * Convolve with a kernel consisting of one row. 344 * @param kernel the kernel 345 * @param inPixels the input pixels 346 * @param outPixels the output pixels 347 * @param width the width 348 * @param height the height 349 * @param alpha include alpha channel 350 * @param edgeAction what to do at the edges 351 */ 352 public static void convolveH(Kernel kernel, int[] inPixels, int[] outPixels, int width, int height, boolean alpha, int edgeAction) { 353 int index = 0; 354 float[] matrix = kernel.getKernelData( null ); 355 int cols = kernel.getWidth(); 356 int cols2 = cols/2; 357 358 for (int y = 0; y < height; y++) { 359 int ioffset = y*width; 360 for (int x = 0; x < width; x++) { 361 float r = 0, g = 0, b = 0, a = 0; 362 int moffset = cols2; 363 for (int col = -cols2; col <= cols2; col++) { 364 float f = matrix[moffset+col]; 365 366 if (f != 0) { 367 int ix = x+col; 368 if ( ix < 0 ) { 369 if ( edgeAction == CLAMP_EDGES ) 370 ix = 0; 371 else if ( edgeAction == WRAP_EDGES ) 372 ix = (x+width) % width; 373 } else if ( ix >= width) { 374 if ( edgeAction == CLAMP_EDGES ) 375 ix = width-1; 376 else if ( edgeAction == WRAP_EDGES ) 377 ix = (x+width) % width; 378 } 379 int rgb = inPixels[ioffset+ix]; 380 a += f * ((rgb >> 24) & 0xff); 381 r += f * ((rgb >> 16) & 0xff); 382 g += f * ((rgb >> 8) & 0xff); 383 b += f * (rgb & 0xff); 384 } 385 } 386 int ia = alpha ? PixelUtils.clamp((int)(a+0.5)) : 0xff; 387 int ir = PixelUtils.clamp((int)(r+0.5)); 388 int ig = PixelUtils.clamp((int)(g+0.5)); 389 int ib = PixelUtils.clamp((int)(b+0.5)); 390 outPixels[index++] = (ia << 24) | (ir << 16) | (ig << 8) | ib; 391 } 392 } 393 } 394 395 /** 396 * Convolve with a kernel consisting of one column. 397 * @param kernel the kernel 398 * @param inPixels the input pixels 399 * @param outPixels the output pixels 400 * @param width the width 401 * @param height the height 402 * @param alpha include alpha channel 403 * @param edgeAction what to do at the edges 404 */ 405 public static void convolveV(Kernel kernel, int[] inPixels, int[] outPixels, int width, int height, boolean alpha, int edgeAction) { 406 int index = 0; 407 float[] matrix = kernel.getKernelData( null ); 408 int rows = kernel.getHeight(); 409 int rows2 = rows/2; 410 411 for (int y = 0; y < height; y++) { 412 for (int x = 0; x < width; x++) { 413 float r = 0, g = 0, b = 0, a = 0; 414 415 for (int row = -rows2; row <= rows2; row++) { 416 int iy = y+row; 417 int ioffset; 418 if ( iy < 0 ) { 419 if ( edgeAction == CLAMP_EDGES ) 420 ioffset = 0; 421 else if ( edgeAction == WRAP_EDGES ) 422 ioffset = ((y+height) % height)*width; 423 else 424 ioffset = iy*width; 425 } else if ( iy >= height) { 426 if ( edgeAction == CLAMP_EDGES ) 427 ioffset = (height-1)*width; 428 else if ( edgeAction == WRAP_EDGES ) 429 ioffset = ((y+height) % height)*width; 430 else 431 ioffset = iy*width; 432 } else 433 ioffset = iy*width; 434 435 float f = matrix[row+rows2]; 436 437 if (f != 0) { 438 int rgb = inPixels[ioffset+x]; 439 a += f * ((rgb >> 24) & 0xff); 440 r += f * ((rgb >> 16) & 0xff); 441 g += f * ((rgb >> 8) & 0xff); 442 b += f * (rgb & 0xff); 443 } 444 } 445 int ia = alpha ? PixelUtils.clamp((int)(a+0.5)) : 0xff; 446 int ir = PixelUtils.clamp((int)(r+0.5)); 447 int ig = PixelUtils.clamp((int)(g+0.5)); 448 int ib = PixelUtils.clamp((int)(b+0.5)); 449 outPixels[index++] = (ia << 24) | (ir << 16) | (ig << 8) | ib; 450 } 451 } 452 } 453 454 public String toString() { 455 return "Blur/Convolve..."; 456 } 457 public BufferedImage filter(BufferedImage src, Struct parameters) throws PageException {BufferedImage dst=ImageUtil.createBufferedImage(src); 458 Object o; 459 if((o=parameters.removeEL(KeyImpl.init("EdgeAction")))!=null)setEdgeAction(ImageFilterUtil.toString(o,"EdgeAction")); 460 if((o=parameters.removeEL(KeyImpl.init("UseAlpha")))!=null)setUseAlpha(ImageFilterUtil.toBooleanValue(o,"UseAlpha")); 461 if((o=parameters.removeEL(KeyImpl.init("PremultiplyAlpha")))!=null)setPremultiplyAlpha(ImageFilterUtil.toBooleanValue(o,"PremultiplyAlpha")); 462 463 // check for arguments not supported 464 if(parameters.size()>0) { 465 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 [Kernel, EdgeAction, UseAlpha, PremultiplyAlpha]"); 466 } 467 468 return filter(src, dst); 469 } 470}