001 package org.apache.tapestry.contrib.services.impl; 002 003 import java.awt.*; 004 import java.awt.image.BufferedImage; 005 import java.awt.image.Raster; 006 import java.awt.image.WritableRaster; 007 008 /** 009 * 010 */ 011 public class ShadowRenderer { 012 013 // size of the shadow in pixels (defines the fuzziness) 014 private int size = 5; 015 016 // opacity of the shadow 017 private float opacity = 0.5f; 018 019 // color of the shadow 020 private Color color = Color.BLACK; 021 022 /** 023 * <p>A shadow renderer needs three properties to generate shadows. 024 * These properties are:</p> 025 * <ul> 026 * <li><i>size</i>: The size, in pixels, of the shadow. This property also 027 * defines the fuzzyness.</li> 028 * <li><i>opacity</i>: The opacity, between 0.0 and 1.0, of the shadow.</li> 029 * <li><i>color</i>: The color of the shadow. Shadows are not meant to be 030 * black only.</li> 031 * </ul> 032 * @param size the size of the shadow in pixels. Defines the fuzziness. 033 * @param opacity the opacity of the shadow. 034 * @param color the color of the shadow. 035 */ 036 public ShadowRenderer(final int size, final float opacity, final Color color) { 037 038 setSize(size); 039 setOpacity(opacity); 040 setColor(color); 041 } 042 043 /** 044 * <p>Gets the color used by the renderer to generate shadows.</p> 045 * @return this renderer's shadow color 046 */ 047 public Color getColor() { 048 return color; 049 } 050 051 /** 052 * <p>Sets the color used by the renderer to generate shadows.</p> 053 * <p>Consecutive calls to {@link #createShadow} will all use this color 054 * until it is set again.</p> 055 * <p>If the color provided is null, the previous color will be retained.</p> 056 * @param shadowColor the generated shadows color 057 */ 058 public void setColor(final Color shadowColor) { 059 if (shadowColor != null) { 060 Color oldColor = this.color; 061 this.color = shadowColor; 062 } 063 } 064 065 /** 066 * <p>Gets the opacity used by the renderer to generate shadows.</p> 067 * <p>The opacity is comprised between 0.0f and 1.0f; 0.0f being fully 068 * transparent and 1.0f fully opaque.</p> 069 * @return this renderer's shadow opacity 070 */ 071 public float getOpacity() { 072 return opacity; 073 } 074 075 /** 076 * <p>Sets the opacity used by the renderer to generate shadows.</p> 077 * <p>Consecutive calls to {@link #createShadow} will all use this opacity 078 * until it is set again.</p> 079 * <p>The opacity is comprised between 0.0f and 1.0f; 0.0f being fully 080 * transparent and 1.0f fully opaque. If you provide a value out of these 081 * boundaries, it will be restrained to the closest boundary.</p> 082 * @param shadowOpacity the generated shadows opacity 083 */ 084 public void setOpacity(final float shadowOpacity) { 085 float oldOpacity = this.opacity; 086 087 if (shadowOpacity < 0.0) { 088 this.opacity = 0.0f; 089 } else if (shadowOpacity > 1.0f) { 090 this.opacity = 1.0f; 091 } else { 092 this.opacity = shadowOpacity; 093 } 094 } 095 096 /** 097 * <p>Gets the size in pixel used by the renderer to generate shadows.</p> 098 * @return this renderer's shadow size 099 */ 100 public int getSize() { 101 return size; 102 } 103 104 /** 105 * <p>Sets the size, in pixels, used by the renderer to generate shadows.</p> 106 * <p>The size defines the blur radius applied to the shadow to create the 107 * fuzziness.</p> 108 * <p>There is virtually no limit to the size. The size cannot be negative. 109 * If you provide a negative value, the size will be 0 instead.</p> 110 * @param shadowSize the generated shadows size in pixels (fuzziness) 111 */ 112 public void setSize(final int shadowSize) { 113 int oldSize = this.size; 114 115 if (shadowSize < 0) { 116 this.size = 0; 117 } else { 118 this.size = shadowSize; 119 } 120 } 121 122 /** 123 * <p>Generates the shadow for a given picture and the current properties 124 * of the renderer.</p> 125 * <p>The generated image dimensions are computed as following:</p> 126 * <pre> 127 * width = imageWidth + 2 * shadowSize 128 * height = imageHeight + 2 * shadowSize 129 * </pre> 130 * @param image the picture from which the shadow must be cast 131 * @return the picture containing the shadow of <code>image</code> 132 */ 133 public BufferedImage createShadow(final BufferedImage image) { 134 135 // Written by Sesbastien Petrucci 136 int shadowSize = size * 2; 137 138 int srcWidth = image.getWidth(); 139 int srcHeight = image.getHeight(); 140 141 int dstWidth = srcWidth + shadowSize; 142 int dstHeight = srcHeight + shadowSize; 143 144 int left = size; 145 int right = shadowSize - left; 146 147 int yStop = dstHeight - right; 148 149 int shadowRgb = color.getRGB() & 0x00FFFFFF; 150 int[] aHistory = new int[shadowSize]; 151 int historyIdx; 152 153 int aSum; 154 155 BufferedImage dst = new BufferedImage(dstWidth, dstHeight, 156 BufferedImage.TYPE_INT_ARGB); 157 158 int[] dstBuffer = new int[dstWidth * dstHeight]; 159 int[] srcBuffer = new int[srcWidth * srcHeight]; 160 161 getPixels(image, 0, 0, srcWidth, srcHeight, srcBuffer); 162 163 int lastPixelOffset = right * dstWidth; 164 float hSumDivider = 1.0f / shadowSize; 165 float vSumDivider = opacity / shadowSize; 166 167 int[] hSumLookup = new int[256 * shadowSize]; 168 for (int i = 0; i < hSumLookup.length; i++) { 169 hSumLookup[i] = (int) (i * hSumDivider); 170 } 171 172 int[] vSumLookup = new int[256 * shadowSize]; 173 for (int i = 0; i < vSumLookup.length; i++) { 174 vSumLookup[i] = (int) (i * vSumDivider); 175 } 176 177 int srcOffset; 178 179 // horizontal pass : extract the alpha mask from the source picture and 180 // blur it into the destination picture 181 for (int srcY = 0, dstOffset = left * dstWidth; srcY < srcHeight; srcY++) { 182 183 // first pixels are empty 184 for (historyIdx = 0; historyIdx < shadowSize; ) { 185 aHistory[historyIdx++] = 0; 186 } 187 188 aSum = 0; 189 historyIdx = 0; 190 srcOffset = srcY * srcWidth; 191 192 // compute the blur average with pixels from the source image 193 for (int srcX = 0; srcX < srcWidth; srcX++) { 194 195 int a = hSumLookup[aSum]; 196 dstBuffer[dstOffset++] = a << 24; // store the alpha value only 197 // the shadow color will be added in the next pass 198 199 aSum -= aHistory[historyIdx]; // substract the oldest pixel from the sum 200 201 // extract the new pixel ... 202 a = srcBuffer[srcOffset + srcX] >>> 24; 203 aHistory[historyIdx] = a; // ... and store its value into history 204 aSum += a; // ... and add its value to the sum 205 206 if (++historyIdx >= shadowSize) { 207 historyIdx -= shadowSize; 208 } 209 } 210 211 // blur the end of the row - no new pixels to grab 212 for (int i = 0; i < shadowSize; i++) { 213 214 int a = hSumLookup[aSum]; 215 dstBuffer[dstOffset++] = a << 24; 216 217 // substract the oldest pixel from the sum ... and nothing new to add ! 218 aSum -= aHistory[historyIdx]; 219 220 if (++historyIdx >= shadowSize) { 221 historyIdx -= shadowSize; 222 } 223 } 224 } 225 226 // vertical pass 227 for (int x = 0, bufferOffset = 0; x < dstWidth; x++, bufferOffset = x) { 228 229 aSum = 0; 230 231 // first pixels are empty 232 for (historyIdx = 0; historyIdx < left;) { 233 aHistory[historyIdx++] = 0; 234 } 235 236 // and then they come from the dstBuffer 237 for (int y = 0; y < right; y++, bufferOffset += dstWidth) { 238 int a = dstBuffer[bufferOffset] >>> 24; // extract alpha 239 aHistory[historyIdx++] = a; // store into history 240 aSum += a; // and add to sum 241 } 242 243 bufferOffset = x; 244 historyIdx = 0; 245 246 // compute the blur avera`ge with pixels from the previous pass 247 for (int y = 0; y < yStop; y++, bufferOffset += dstWidth) { 248 249 int a = vSumLookup[aSum]; 250 dstBuffer[bufferOffset] = a << 24 | shadowRgb; // store alpha value + shadow color 251 252 aSum -= aHistory[historyIdx]; // substract the oldest pixel from the sum 253 254 a = dstBuffer[bufferOffset + lastPixelOffset] >>> 24; // extract the new pixel ... 255 aHistory[historyIdx] = a; // ... and store its value into history 256 aSum += a; // ... and add its value to the sum 257 258 if (++historyIdx >= shadowSize) { 259 historyIdx -= shadowSize; 260 } 261 } 262 263 // blur the end of the column - no pixels to grab anymore 264 for (int y = yStop; y < dstHeight; y++, bufferOffset += dstWidth) { 265 266 int a = vSumLookup[aSum]; 267 dstBuffer[bufferOffset] = a << 24 | shadowRgb; 268 269 aSum -= aHistory[historyIdx]; // substract the oldest pixel from the sum 270 271 if (++historyIdx >= shadowSize) { 272 historyIdx -= shadowSize; 273 } 274 } 275 } 276 277 setPixels(dst, 0, 0, dstWidth, dstHeight, dstBuffer); 278 return dst; 279 } 280 281 /** 282 * <p>Returns an array of pixels, stored as integers, from a 283 * <code>BufferedImage</code>. The pixels are grabbed from a rectangular 284 * area defined by a location and two dimensions. Calling this method on 285 * an image of type different from <code>BufferedImage.TYPE_INT_ARGB</code> 286 * and <code>BufferedImage.TYPE_INT_RGB</code> will unmanage the image.</p> 287 * 288 * @param img the source image 289 * @param x the x location at which to start grabbing pixels 290 * @param y the y location at which to start grabbing pixels 291 * @param w the width of the rectangle of pixels to grab 292 * @param h the height of the rectangle of pixels to grab 293 * @param pixels a pre-allocated array of pixels of size w*h; can be null 294 * @return <code>pixels</code> if non-null, a new array of integers 295 * otherwise 296 * @throws IllegalArgumentException is <code>pixels</code> is non-null and 297 * of length < w*h 298 */ 299 public static int[] getPixels(BufferedImage img, 300 int x, int y, int w, int h, int[] pixels) { 301 if (w == 0 || h == 0) { 302 return new int[0]; 303 } 304 305 if (pixels == null) { 306 pixels = new int[w * h]; 307 } else if (pixels.length < w * h) { 308 throw new IllegalArgumentException("pixels array must have a length" + 309 " >= w*h"); 310 } 311 312 int imageType = img.getType(); 313 if (imageType == BufferedImage.TYPE_INT_ARGB || 314 imageType == BufferedImage.TYPE_INT_RGB) { 315 Raster raster = img.getRaster(); 316 return (int[]) raster.getDataElements(x, y, w, h, pixels); 317 } 318 319 // Unmanages the image 320 return img.getRGB(x, y, w, h, pixels, 0, w); 321 } 322 323 /** 324 * <p>Writes a rectangular area of pixels in the destination 325 * <code>BufferedImage</code>. Calling this method on 326 * an image of type different from <code>BufferedImage.TYPE_INT_ARGB</code> 327 * and <code>BufferedImage.TYPE_INT_RGB</code> will unmanage the image.</p> 328 * 329 * @param img the destination image 330 * @param x the x location at which to start storing pixels 331 * @param y the y location at which to start storing pixels 332 * @param w the width of the rectangle of pixels to store 333 * @param h the height of the rectangle of pixels to store 334 * @param pixels an array of pixels, stored as integers 335 * @throws IllegalArgumentException is <code>pixels</code> is non-null and 336 * of length < w*h 337 */ 338 public static void setPixels(BufferedImage img, 339 int x, int y, int w, int h, int[] pixels) { 340 if (pixels == null || w == 0 || h == 0) { 341 return; 342 } else if (pixels.length < w * h) { 343 throw new IllegalArgumentException("pixels array must have a length" + 344 " >= w*h"); 345 } 346 347 int imageType = img.getType(); 348 if (imageType == BufferedImage.TYPE_INT_ARGB || 349 imageType == BufferedImage.TYPE_INT_RGB) { 350 WritableRaster raster = img.getRaster(); 351 raster.setDataElements(x, y, w, h, pixels); 352 } else { 353 // Unmanages the image 354 img.setRGB(x, y, w, h, pixels, 0, w); 355 } 356 } 357 }