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 &lt; 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 &lt; 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    }