View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   * --
19   * $Id: RoundedBorderGenerator.java 554270 2007-07-07 21:33:42Z arobinson74 $
20   */
21  package org.apache.myfaces.custom.roundeddiv;
22  
23  import java.awt.AlphaComposite;
24  import java.awt.BasicStroke;
25  import java.awt.Color;
26  import java.awt.Dimension;
27  import java.awt.Graphics2D;
28  import java.awt.Polygon;
29  import java.awt.RenderingHints;
30  import java.awt.Shape;
31  import java.awt.Transparency;
32  import java.awt.geom.Arc2D;
33  import java.awt.geom.RoundRectangle2D;
34  import java.awt.image.BufferedImage;
35  import java.io.File;
36  import java.io.FileOutputStream;
37  
38  import javax.imageio.ImageIO;
39  
40  /**
41   * Class that generates rounded images
42   * 
43   * @author Andrew Robinson (latest modification by $Author: arobinson74 $)
44   * @version $Revision: 554270 $ $Date: 2007-07-07 23:33:42 +0200 (Sat, 07 Jul 2007) $
45   */
46  public class RoundedBorderGenerator
47  {
48      public final static int SECTION_TOPLEFT = 1;
49      public final static int SECTION_TOP = 2;
50      public final static int SECTION_TOPRIGHT = 3;
51      public final static int SECTION_LEFT = 4;
52      public final static int SECTION_CENTER = 5;
53      public final static int SECTION_RIGHT = 6;
54      public final static int SECTION_BOTTOMLEFT = 7;
55      public final static int SECTION_BOTTOM = 8;
56      public final static int SECTION_BOTTOMRIGHT = 9;
57      
58      private final static int CORNER_DEGREES = 3;
59  
60      private transient BufferedImage cachedImage;
61      private int borderWidth;
62      private int cornerRadius;
63      private Color color;
64      private Color background;
65      private Color borderColor;
66      private Dimension size;
67      private boolean inverse;
68  
69      /**
70       * Constructor 
71       * 
72       * @param borderColor Border color
73       * @param borderWidth Border width
74       * @param cornerRadius Corner radius
75       * @param color Foreground color
76       * @param background Background color
77       * @param size Image size
78       * @param inverse if the 3D border should be inverse (depressed look)
79       */
80      public RoundedBorderGenerator(Color borderColor, int borderWidth,
81              int cornerRadius, Color color, Color background, Dimension size,
82              boolean inverse)
83      {
84          this.borderWidth = borderWidth;
85          this.cornerRadius = cornerRadius;
86          this.color = color;
87          this.background = background;
88          this.borderColor = borderColor;
89          this.size = size;
90          this.inverse = inverse;
91      }
92  
93      public void dispose()
94      {
95          cachedImage = null;
96      }
97  
98      /**
99       * @return the inverse
100      */
101     public boolean isInverse()
102     {
103         return this.inverse;
104     }
105 
106     /**
107      * @return the background
108      */
109     public Color getBackground()
110     {
111         return this.background;
112     }
113 
114     /**
115      * @return the borderColor
116      */
117     public Color getBorderColor()
118     {
119         return this.borderColor;
120     }
121 
122     /**
123      * @return the borderWidth
124      */
125     public int getBorderWidth()
126     {
127         return this.borderWidth;
128     }
129 
130     /**
131      * @return the color
132      */
133     public Color getColor()
134     {
135         return this.color;
136     }
137 
138     /**
139      * @return the cornerRadius
140      */
141     public int getCornerRadius()
142     {
143         return this.cornerRadius;
144     }
145 
146     /**
147      * @return the size
148      */
149     public Dimension getSize()
150     {
151         return this.size;
152     }
153 
154     public BufferedImage createImageSection(int section)
155     {
156         BufferedImage img = createImage();
157 
158         int max = Math.max(borderWidth, cornerRadius);
159 
160         int x, y;
161         switch (section)
162         {
163         case SECTION_CENTER:
164             x = y = max;
165             break;
166         case SECTION_TOPLEFT:
167             x = y = 0;
168             break;
169         case SECTION_TOP:
170             x = max;
171             y = 0;
172             break;
173         case SECTION_TOPRIGHT:
174             x = img.getWidth() - max;
175             y = 0;
176             break;
177         case SECTION_RIGHT:
178             x = img.getWidth() - max;
179             y = max;
180             break;
181         case SECTION_BOTTOMRIGHT:
182             x = img.getWidth() - max;
183             y = img.getHeight() - max;
184             break;
185         case SECTION_BOTTOM:
186             x = max;
187             y = img.getHeight() - max;
188             break;
189         case SECTION_BOTTOMLEFT:
190             x = 0;
191             y = img.getHeight() - max;
192             break;
193         case SECTION_LEFT:
194         default:
195             x = 0;
196             y = max;
197             break;
198         }
199 
200         return img.getSubimage(x, y, max, max);
201     }
202 
203     public BufferedImage createImage()
204     {
205         if (cachedImage != null)
206             return cachedImage;
207 
208         // create the canvas image
209         int w, h;
210         if (size != null)
211         {
212             w = (int) size.getWidth();
213             h = (int) size.getHeight();
214         }
215         else
216         {
217             h = w = Math.max(borderWidth, cornerRadius) * 3;
218         }
219 
220         BufferedImage canvas = new BufferedImage(w, h,
221                 BufferedImage.TYPE_INT_ARGB);
222         Graphics2D g = canvas.createGraphics();
223         Graphics2D g2 = null;
224 
225         try
226         {
227             // either paint the background of the image, or set it to transparent
228             paintBackground(g, w, h);
229 
230             RoundRectangle2D shape = new RoundRectangle2D.Float(0f, 0f, w, h,
231                     cornerRadius * 2, cornerRadius * 2);
232 
233             BufferedImage shapedImage = createImageForShape(g, shape);
234             g2 = shapedImage.createGraphics();
235 
236             fillForeground(g2, shape);
237 
238             // 3D or 2D?
239             if (borderColor == null)
240                 create3DImage(g2, shape);
241             else
242                 create2DImage(g2, shape);
243 
244             g.drawImage(shapedImage, 0, 0, null);
245 
246             cachedImage = canvas;
247             return canvas;
248         }
249         finally
250         {
251             if (g2 != null)
252                 try
253                 {
254                     g2.dispose();
255                 }
256                 catch (Exception ex)
257                 {
258                 }
259             g.dispose();
260         }
261     }
262 
263     private BufferedImage createImageForShape(Graphics2D g,
264             RoundRectangle2D shape)
265     {
266         int w = shape.getBounds().width;
267         int h = shape.getBounds().height;
268 
269         BufferedImage img = g.getDeviceConfiguration().createCompatibleImage(w,
270                 h, Transparency.TRANSLUCENT);
271         Graphics2D g2 = img.createGraphics();
272 
273         try
274         {
275             // Clear the image so all pixels have zero alpha
276             g2.setComposite(AlphaComposite.Clear);
277             g2.fillRect(0, 0, w, h);
278 
279             // Render our clip shape into the image. Note that we enable
280             // antialiasing to achieve the soft clipping effect. Try
281             // commenting out the line that enables antialiasing, and
282             // you will see that you end up with the usual hard clipping.
283             g2.setComposite(AlphaComposite.Src);
284             g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
285                     RenderingHints.VALUE_ANTIALIAS_ON);
286             g2.setColor(Color.WHITE);
287             g2.fill(shape);
288         }
289         finally
290         {
291             g2.dispose();
292         }
293 
294         return img;
295     }
296 
297     private void paintBackground(Graphics2D g, int width, int height)
298     {
299         // treat a null background as fully transparent
300         if (background == null)
301         {
302             BufferedImage img = g.getDeviceConfiguration()
303                     .createCompatibleImage(width, height,
304                             Transparency.TRANSLUCENT);
305             Graphics2D g2 = img.createGraphics();
306 
307             // Clear the image so all pixels have zero alpha
308             g2.setComposite(AlphaComposite.Clear);
309             g2.fillRect(0, 0, width, height);
310 
311             g2.dispose();
312         }
313         else
314         {
315             g.setColor(background);
316             g.fillRect(0, 0, width, height);
317         }
318     }
319 
320     private void fillForeground(Graphics2D g, RoundRectangle2D shape)
321     {
322         // use anti-aliasing, looks like garbage without it
323         g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
324                 RenderingHints.VALUE_ANTIALIAS_ON);
325         g.setComposite(AlphaComposite.SrcAtop);
326         g.setPaint(color);
327         g.fill(shape);
328     }
329 
330     /**
331      * Create a 3D lighting effect using the "computer standard" light from the upper left
332      */
333     private void create3DImage(Graphics2D g, RoundRectangle2D shape)
334     {
335         int w = (int) shape.getWidth();
336         int h = (int) shape.getHeight();
337 
338         // Create a 3D lighting effect on the border of the rectangle
339         float factor = .3f;
340         Color lighter = lighter(color, factor);
341         Color darker = darker(color, factor);
342 
343         // top-left
344         g.setClip(new Polygon(new int[] { 0, w, 0 }, new int[] { 0, 0, h }, 3));
345         paint3DBorder(g, inverse ? darker : lighter, shape);
346 
347         // bottom-right
348         g.setClip(new Polygon(new int[] { 0, w, w }, new int[] { h, 0, h }, 3));
349         paint3DBorder(g, inverse ? lighter : darker, shape);
350 
351         // create a transition at the top-right and bottom-left colors to blend
352         // where the darker & lighter colors meet
353         g.setClip(null);
354         int corner = Math.max(borderWidth, cornerRadius);
355         paint3DBorderTransition(g, inverse ? darker : lighter,
356                 inverse ? lighter : darker, w - corner * 2, 0, true);
357         paint3DBorderTransition(g, inverse ? darker : lighter,
358                 inverse ? lighter : darker, 0, h - corner * 2, false);
359         g.setClip(null);
360     }
361 
362     private void paint3DBorderTransition(Graphics2D g, Color c1, Color c2,
363             int x, int y, boolean upperLeft)
364     {
365         // starting with a double-width stroke, use an algorithm that stays closer to the
366         // middle color at first and then increasingly approaches outside (lighter or darker)
367         // towards the end to simulate the light increasing or fading at the edges
368         // (the image will clip any content outside of its bounds, so half of each stroke is lost)
369         int width = borderWidth * 2;
370         int size = Math.max(borderWidth, cornerRadius);
371         
372         int startAngle = upperLeft ? 0 : 180;
373         
374         for (int i = 0; i < 90; i += CORNER_DEGREES)
375         {
376             Color outerColor = combineColors(c1, c2, (upperLeft ? i : 90 - i) / 90f);
377             g.setClip(new Arc2D.Float(x, y, size * 2, size * 2,
378                     startAngle + i, CORNER_DEGREES, Arc2D.PIE));
379             paint3DBorder(g, outerColor, new Arc2D.Float(x, y, size * 2, size * 2,
380                     startAngle + i, CORNER_DEGREES, Arc2D.OPEN));
381         }
382     }
383 
384     private void paint3DBorder(Graphics2D g, Color c, Shape shape)
385     {
386         // starting with a double-width stroke, use an algorithm that stays closer to the
387         // middle color at first and then increasingly approaches outside (lighter or darker)
388         // towards the end to simulate the light increasing or fading at the edges
389         // (the image will clip any content outside of its bounds, so half of each stroke is lost)
390         int width = borderWidth * 2;
391         for (float i = 0; i <= width; i += 1)
392         {
393             float percent = (float) Math.pow((i / width), 3);
394             g.setPaint(combineColors(c, color, percent));
395             g.setStroke(new BasicStroke(width - i));
396             g.draw(shape);
397         }
398     }
399 
400     private void create2DImage(Graphics2D g, RoundRectangle2D shape)
401     {
402         if (borderWidth == 0)
403             return;
404         // put the border inside the shape
405         RoundRectangle2D borderShape = new RoundRectangle2D.Float(0f, 0f,
406                 (float) shape.getWidth() - 1, (float) shape.getHeight() - 1,
407                 cornerRadius * 2, cornerRadius * 2);
408 
409         g.setStroke(new BasicStroke(borderWidth));
410         g.setColor(borderColor);
411         g.draw(borderShape);
412     }
413 
414     /**
415      * Averages the red, green, blue and alpha of two colors
416      */
417     private Color combineColors(Color c1, Color c2, float weight)
418     {
419         float r = c1.getRed() * weight + c2.getRed() * (1 - weight);
420         float g = c1.getGreen() * weight + c2.getGreen() * (1 - weight);
421         float b = c1.getBlue() * weight + c2.getBlue() * (1 - weight);
422         float a = c1.getAlpha() * weight + c2.getAlpha() * (1 - weight);
423         return new Color((int) r, (int) g, (int) b, (int) a);
424     }
425 
426     /**
427      * Similar to {@link Color#brighter()}, but this method lightens by bringing the color 
428      * closer to white (not just making it brighter) by using a custom factor (instead of 0.7)
429      * 
430      * @param c the color to lighten
431      * @param factor the factor to use (between 0 and 1)
432      * @return the lighter color
433      */
434     private Color lighter(Color c, float factor)
435     {
436         return new Color(Math
437                 .min((int) (Math.max(c.getRed(), 50) / factor), 255), Math.min(
438                 (int) (Math.max(c.getGreen(), 50) / factor), 255), Math.min(
439                 (int) (Math.max(c.getBlue(), 50) / factor), 255));
440     }
441 
442     /**
443      * Like {@link Color#darker()}, this method darkens a color, but with a custom factor 
444      * (instead of 0.7)
445      * 
446      * @param c the color to darken
447      * @param factor the factor to use (between 0 and 1)
448      * @return the darker color
449      */
450     private Color darker(Color c, float factor)
451     {
452         return new Color(Math.max((int) (c.getRed() * factor), 0), Math.max(
453                 (int) (c.getGreen() * factor), 0), Math.max(
454                 (int) (c.getBlue() * factor), 0));
455     }
456 
457     public static void main(String[] args)
458     {
459         FileOutputStream fos = null;
460 
461         try
462         {
463             File file;
464             if (args.length == 1)
465             {
466                 file = new File(args[0]);
467             }
468             else
469             {
470                 file = File.createTempFile("tmp_", ".png");
471             }
472 
473             RoundedBorderGenerator rbg = new RoundedBorderGenerator(null, 10,
474                     10, Color.GRAY, null, new Dimension(120,
475                             60), false);
476             fos = new FileOutputStream(file);
477 
478             ImageIO.write(rbg.createImage(), "png", fos);
479 
480             System.out.println("written to " + file.getAbsolutePath());
481         }
482         catch (Exception ex)
483         {
484             ex.printStackTrace();
485             System.exit(1);
486         }
487         finally
488         {
489             if (fos != null)
490             {
491                 try
492                 {
493                     fos.close();
494                 }
495                 catch (Exception ex)
496                 {
497                 }
498             }
499         }
500     }
501 }