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