Saturday, December 29, 2007

Creating PNG Images with Rounded Corners

I have been working with the image functions in ColdFusion 8 and trying to figure out how to create rounded corners using an existing image.

First let me point out there are some impressive professional products available that can perform this feat, such as Image Effects by Foundeo Inc. Obviously it is more robust and better suited to handling high resolution, high quality images, than the technique described here ;) As my image needs were more basic, I decided to investigate if this were possible using only ColdFusion's built in image functions.

Searching the documentation I found the ImageDrawRoundRect function. It looked promising but it appears to work only for drawing new shapes. What I needed was a way to clip the drawing area of an existing shape. Not having found a suitable combination of CF image functions yet, I decided to try my hand at creating a function that used java directly.

Clipping


My first inclination was to try clipping. Using the same dimensions as the original image, I created a BufferedImage that supports transparency.

<cfscript>
Local.BufferedImage = createObject("java", "java.awt.image.BufferedImage");
Local.destImage = Local.BufferedImage.init(
javacast("int", Local.width),
javacast("int", Local.height),
Local.BufferedImage.TYPE_INT_ARGB
);
</cfscript>


I then used a rounded rectangle to define the clipping area, so anything outside the bounds of the rectangle would appear transparent.
<cfscript>
Local.graphics = Local.destImage.createGraphics();
Local.RoundRectangle2D = createObject("java", "java.awt.geom.RoundRectangle2D$Double");
Local.rounded = Local.RoundRectangle2D.init( javacast("double", 0),
javacast("double", 0),
javacast("double", Local.width),
javacast("double", Local.height),
javacast("double", arguments.arcWidth),
javacast("double", arguments.arcHeight)
);
Local.graphics.setClip( Local.rounded );
</cfscript>


Finally I drew the original image onto the new BufferedImage. Then passed the BufferedImage into the ImageNew() function to return a CF compatible image object.

<cfscript>
Local.graphics.drawImage( Local.sourceImage,
javacast("int", 0),
javacast("int", 0),
javacast("null", "")
);
Local.graphics.dispose();
</cfscript>

<cfreturn ImageNew( Local.destImage )>


As you can see it did work. But if you look closely at the image on the right, the rounded edges appear ragged. At least on the windows o/s.



If at first you don't succeed


Next I tried a tip from the sun.com forums. It suggested using a mask to do the equivalent of clipping, but with better blending of the edges. So this time I used anti-aliasing to draw the rounded rectangle shape first.

Update: This article gives a good explanation of why the mask technique works, as it involves more than just using the anti-aliasing hint.
http://weblogs.java.net/blog/campbell/archive/2006/07/java_2d_tricker.html

<cfscript>
// cfSearching: use anti-aliasing for smoother edges
Local.RenderingHints = createObject("java", "java.awt.RenderingHints");
Local.graphics.setRenderingHint( Local.RenderingHints.KEY_ANTIALIASING,
Local.RenderingHints.VALUE_ANTIALIAS_ON
);
// cfSearching: create a rounded rectangle mask
Local.graphics.setColor( Local.Color.WHITE );
Local.graphics.fillRoundRect(javacast("double", 0),
javacast("double", 0),
javacast("double", Local.width),
javacast("double", Local.height),
javacast("double", arguments.arcWidth),
javacast("double", arguments.arcHeight)
);
</cfscript>


Then used a Source-in AlphaComposite to draw the original image over the rectangle mask. Again the end result being everything outside the rectangle area appears transparent. If you are not familiar with compositing rules, take a peek at one of the sun.com tutorials. The image examples clearly demonstrate the effect of the different AlphaComposite rules.

<cfscript>
// cfSeaching: draw the source image onto the mask
Local.graphics.setComposite( Local.AlphaComposite.SrcIn );
Local.graphics.drawImage( Local.sourceImage,
javacast("int", 0),
javacast("int", 0),
javacast("null", "")
);
Local.graphics.dispose();
</cfscript>


As you can see the resulting corners are much smoother this time.



Conclusion


Now there is obviously much more to handling graphics than I covered here. But hopefully this simple technique demonstrates a few of the things you can do with BufferedImages. As always, comments/corrections/suggestions are welcome. Enjoy!


Clipping Example (Ragged Edges)
<h1>Rounded Corners (Clip) Example</h1>

<cfset fullPathToSourceImage = ExpandPath("testImage3.jpg")>
<!--- display original image --->
<cfoutput>
<img src="#GetFileFromPath(fullPathToSourceImage)#"><br><br>
</cfoutput>

<!--- display new image with rounded corners (arc 50 x 50) --->
<cfset roundedImage = roundedCornersClip( fullPathToSourceImage, 50, 50 )>
<cfimage source="#roundedImage#" action="WriteToBrowser" format="png">


<cffunction name="roundedCornersClip" returntype="any" access="public" output="false">
<cfargument name="source" type="string" required="true" hint="full path to source image">
<cfargument name="arcWidth" type="numeric" required="true" hint="horizontal diameter of arc">
<cfargument name="arcHeight" type="numeric" required="true" hint="vertical diameter of arc">
<cfset var Local = structNew()>

<cfscript>
Local.sourceImage = ImageGetBufferedImage(ImageNew(arguments.source));
Local.width  = Local.sourceImage.getWidth();
Local.height = Local.sourceImage.getHeight();

Local.BufferedImage = createObject("java", "java.awt.image.BufferedImage");
Local.destImage = Local.BufferedImage.init(
javacast("int", Local.width),
javacast("int", Local.height),
Local.BufferedImage.TYPE_INT_ARGB
);

Local.graphics = Local.destImage.createGraphics();
Local.RoundRectangle2D = createObject("java", "java.awt.geom.RoundRectangle2D$Double");
Local.rounded = Local.RoundRectangle2D.init( javacast("double", 0),
javacast("double", 0),
javacast("double", Local.width),
javacast("double", Local.height),
javacast("double", arguments.arcWidth),
javacast("double", arguments.arcHeight)
);

Local.graphics.setClip( Local.rounded );  
Local.graphics.drawImage( Local.sourceImage,
javacast("int", 0),
javacast("int", 0),
javacast("null", "")
);
Local.graphics.dispose();
</cfscript>

<cfreturn ImageNew( Local.destImage )>
</cffunction>


Mask Example (Smoother Edges)
<h1>Rounded Corners (Mask) Example</h1>

<cfset fullPathToSourceImage = ExpandPath("testImage3.jpg")>

<!--- display original image --->
<cfoutput>
<img src="#GetFileFromPath(fullPathToSourceImage)#"><br><br>
</cfoutput>

<!--- display new image with rounded corners (arc 50 x 50) --->
<cfset roundedImage = roundedCornersMask( fullPathToSourceImage, 50, 50 )>
<cfimage source="#roundedImage#" action="WriteToBrowser" format="png">

<cffunction name="roundedCornersMask" returntype="any" access="public" output="false">
<cfargument name="source" type="string" required="true" hint="full path to source image">
<cfargument name="arcWidth" type="numeric" required="true" hint="horizontal diameter of the rounded corners">
<cfargument name="arcHeight" type="numeric" required="true" hint="vertical diameter of the rounded corners">

<cfscript>
var Local = structNew();

// cfSearching: create required java objects
Local.Color = createObject("java", "java.awt.Color");
Local.AlphaComposite = createObject("java", "java.awt.AlphaComposite");

Local.sourceImage = ImageGetBufferedImage(ImageNew(arguments.source));
Local.width  = Local.sourceImage.getWidth();
Local.height = Local.sourceImage.getHeight();

// cfSearching: create a bufferedImage to hold the mask
Local.BufferedImage = createObject("java", "java.awt.image.BufferedImage");
Local.Mask = Local.BufferedImage.init( javacast("int", Local.width),
javacast("int", Local.height),
Local.BufferedImage.TYPE_INT_ARGB
);
Local.graphics = Local.Mask.createGraphics();

// cfSearching: use anti-aliasing for smoother edges
Local.RenderingHints = createObject("java", "java.awt.RenderingHints");
Local.graphics.setRenderingHint( Local.RenderingHints.KEY_ANTIALIASING,
Local.RenderingHints.VALUE_ANTIALIAS_ON
);
// cfSearching: create a rounded rectangle mask
Local.graphics.setColor( Local.Color.WHITE );
Local.graphics.fillRoundRect(javacast("double", 0),
javacast("double", 0),
javacast("double", Local.width),
javacast("double", Local.height),
javacast("double", arguments.arcWidth),
javacast("double", arguments.arcHeight)
);

// cfSeaching: draw the source image onto the mask
Local.graphics.setComposite( Local.AlphaComposite.SrcIn );
Local.graphics.drawImage( Local.sourceImage,
javacast("int", 0),
javacast("int", 0),
javacast("null", "")
);
Local.graphics.dispose();
</cfscript>

<cfreturn ImageNew(Local.Mask)>
</cffunction>


15 comments:

Joe,  July 24, 2008 at 2:47 PM  

You seriously rock. Thank you for providing a free way to round corners in CF8!

cfSearching July 25, 2008 at 2:45 PM  

@Joe,

You are welcome! I was excited when I finally figured it out and thought someone else might find the tip useful. Always nice to know someone did ;-)

Cheers

J. Nelson July 25, 2008 at 2:52 PM  

I'm looking to do the same thing myself, but I don't have CF8 up and running yet (we're getting a new server and waiting until then to buy 8). So, I was wondering if you've tried drawing a rounded rectangle and then overlaying your image using ImageOverlay(). It sounds like that might work, but without having a running CF8 server to test on, I'm not sure. Care to test this and find out?

-John

cfSearching July 25, 2008 at 8:50 PM  

@J. Nelson,

Yes, I think that is one of the things I tried. IIRC it did not work. The effect was like drawing a large opaque rectangle on top of a small circle. Since the rectangle is opaque, nothing happens except that it hides the smaller circle.

But I will give it another whirl to be certain ;-)

cfSearching July 27, 2008 at 8:00 PM  

@J. Nelson,

Yes, it does not work. To achieve the rounded corners you need to clip (or remove) certain areas of the base image. Unfortunately, overlaying does not do that.

portablog May 22, 2009 at 2:29 AM  

This is brilliant, I love blogs like this.

Thank you very much.

Cookie

Anonymous,  July 24, 2009 at 8:48 AM  

I never ever post... but here I had to. You seriously rock dude. Kudos!! Thank you so much for this.

Bradley,  December 23, 2009 at 11:18 AM  

Full of AwesomeSauce! Thanks.

FYI / FWIW, I found that changing this line

Local.sourceImage = ImageGetBufferedImage(ImageNew(arguments.source));

to this

Local.sourceImage = ImageGetBufferedImage(arguments.source);

also allowed me to pass an image to your routine as an already loaded object. Handy if you are applying cumulative effects to an image.

cfSearching December 23, 2009 at 12:04 PM  

@Bradley,

Good tip! Thanks for sharing.

-Leigh

Gary Gilbert January 5, 2010 at 2:06 AM  

Thanks a bunch for posting this, I was just about to start "re-inventing" the wheel here and stumbled upon your blog entry saving me all the work of doing it myself.

That again!

cfSearching January 5, 2010 at 7:27 AM  

@Gary,

Considering some of the entries on your blog have saved me a lot of time and effort, I am glad I could return the favor ;-)

-Leigh

gary gilbert January 7, 2010 at 2:36 AM  

@Leigh,

Glad I could help you.

I do have one question though. Have you tried to used the RoundRectangle2D class to get a rounded corner on a jpg image. I've spent a little bit of time trying to get it working but havnt had much luck yet.

Client wants jpg images not png's basically a white rounded border overlay on the image...Any help or points in the right direction would be appreciated.

cfSearching January 7, 2010 at 1:28 PM  

@Gary,

I know you found an answer to this question already. But I am looking forward to reading your blog entry on it. Please feel free to post a link here when it is ready for reader consumption!

Cheers
-Leigh

Gary Gilbert January 8, 2010 at 12:50 AM  

@Leigh,

here is the blog entry I wrote, it's mostly your code but with some additions to return a jpg image when the source is a jpg.

http://www.garyrgilbert.com/blog/index.cfm/2010/1/7/Creating-JPG-Images-with-Rounded-Corners

  © Blogger templates The Professional Template by Ourblogtemplates.com 2008

Header image adapted from atomicjeep