Posted September 28, 2007 at
6:18 PM
Tags:
ColdFusion
I wanted to give Barney Boisvert's CFImage contest a try, but unfortunately, this week has just beat on me like I owed it money. However, after looking at Barney's posted solution, there is no way that I would have been able to solve the problem anyway. It involved grabbing the underlying Java AWT graphics object and then using that to get font information and this was way out of my league.
Then, today, when he published his Extra Credit Solution, it totally inspired me. Barney, being the mad scientist that he is, filled my head with a whole lot of ColdFusion know-how, and I just had to put it to good use. And, while my use of ColdFusion 8 image manipulation functionality has not been very extensive, one thing that bothered me very quickly was the text handling. It just doesn't feel very intuitive or natural compared to HTML.
To overcome this deficiency, I have created a ColdFusion 8 user defined function, ImageDrawTextArea(), that works just like the builtin ColdFusion 8 ImageDrawText() method with the added features that you can specify a text area width, line-height, and text alignment. The real winner here, though, is the Width parameter. Being a text area rather than just a line of text, it does what you suspect it might: it wraps the text in such a way that it will fit in the given width.
In addition to the existing font attributes, the TextAlign attribute allows you to left, center, and right justify the text. The LineHeight attribute allows you to control the spacing of the individual lines as they wrap. To give you an idea of how this works, take a look at this snippet of code:
Launch code in new window » Download code as text file »
- <cfset ImageDrawTextArea(
- imgCanvas,
- "If you think flattery will get you anywhere with me, well... that's where you're right.",
- 50,
- 50,
- 400,
- objAttributes
- ) />
-
- <cfimage
- action="writetobrowser"
- source="#imgCanvas#"
- />
Notice here that we are not breaking up our text in anyway - we are just passing it in as one long string. Then, with our 5th method parameters (value of 400), we are defining the width of the rendered text area (in which text wrapping will happen automatically). Running the above code (incomplete snippet), we get the following ColdFusion 8 image:
Notice that not only did the text wrap automatically, but it is also right-aligned in the text area. You cannot see it in the above snippet, but this is due to the TextAlign attribute being set.
ColdFusion 8 Online Demo: Try ImageDrawTextArea() out for yourself.
Before I output the code, I just wanna give Barney some more praise cause the dude is brilliant and I just would not have been able to figure any of this out without picking through his code (and adding a billion comments ;)). So anyway, here is code for ImageDrawTextArea(), my ColdFusion 8 user defined function:
Launch code in new window » Download code as text file »
- <cffunction
- name="ImageDrawTextArea"
- access="public"
- returntype="void"
- output="true"
- hint="Draws a text area on the given canvas.">
-
- <cfargument
- name="Source"
- type="any"
- required="true"
- hint="The image on which we are going to write the text."
- />
-
- <cfargument
- name="Text"
- type="string"
- required="true"
- hint="The text value that we are going to write."
- />
-
- <cfargument
- name="X"
- type="numeric"
- required="true"
- hint="The X coordinate of the start of the text."
- />
-
- <cfargument
- name="Y"
- type="numeric"
- required="true"
- hint="The Y coordinate of the baseline of the start of the text."
- />
-
- <cfargument
- name="Width"
- type="numeric"
- required="true"
- hint="The width of the text area in which the text should fit."
- />
-
- <cfargument
- name="Attributes"
- type="struct"
- required="false"
- default="#StructNew()#"
- hint="The attributes of the font (including TextAlign and LineHeight)."
- />
-
-
- <cfset var LOCAL = {} />
-
-
- <cfset LOCAL.Graphics = ImageGetBufferedImage( ARGUMENTS.Source ).GetGraphics() />
-
- <cfset LOCAL.CurrentFont = LOCAL.Graphics.GetFont() />
-
-
-
-
- <cfif NOT StructKeyExists( ARGUMENTS.Attributes, "Size" )>
-
- <cfset ARGUMENTS.Attributes.Size = LOCAL.CurrentFont.GetSize() />
-
- </cfif>
-
-
- <cfif NOT StructKeyExists( ARGUMENTS.Attributes, "Font" )>
-
- <cfset ARGUMENTS.Attributes.Font = LOCAL.CurrentFont.GetFontName() />
-
- </cfif>
-
-
- <cfif NOT StructKeyExists( ARGUMENTS.Attributes, "Style" )>
-
- <cfif (
- LOCAL.CurrentFont.IsBold() AND
- LOCAL.CurrentFont.IsItalic()
- )>
-
- <cfset ARGUMENTS.Attributes.Style = "bolditalic" />
-
- <cfset LOCAL.FontStyleMask = BitOR(
- LOCAL.CurrentFont.BOLD,
- LOCAL.CurrentFont.ITALIC
- ) />
-
- <cfelseif LOCAL.CurrentFont.IsBold()>
-
- <cfset ARGUMENTS.Attributes.Style = "bold" />
-
- <cfset LOCAL.FontStyleMask = LOCAL.CurrentFont.BOLD />
-
- <cfelseif LOCAL.CurrentFont.IsItalic()>
-
- <cfset ARGUMENTS.Attributes.Style = "italic" />
-
- <cfset LOCAL.FontStyleMask = LOCAL.CurrentFont.ITALIC />
-
- <cfelse>
-
- <cfset ARGUMENTS.Attributes.Style = "plain" />
-
- <cfset LOCAL.FontStyleMask = LOCAL.CurrentFont.PLAIN />
-
- </cfif>
-
- <cfelse>
-
- <cfset LOCAL.FontStyleMask = LOCAL.CurrentFont.PLAIN />
-
- </cfif>
-
-
- <cfset LOCAL.NewFont = CreateObject(
- "java",
- "java.awt.Font"
- ).Init(
- JavaCast( "string", ARGUMENTS.Attributes.Font ),
- JavaCast( "int", LOCAL.FontStyleMask ),
- JavaCast( "int", ARGUMENTS.Attributes.Size )
- )
- />
-
-
-
-
- <cfparam
- name="ARGUMENTS.Attributes.LineHeight"
- type="numeric"
- default="#(1.4 * ARGUMENTS.Attributes.Size)#"
- />
-
- <cfparam
- name="ARGUMENTS.Attributes.TextAlign"
- type="string"
- default="left"
- />
-
-
-
- <cfset LOCAL.FontMetrics = LOCAL.Graphics.GetFontMetrics(
- LOCAL.NewFont
- ) />
-
-
- <cfset LOCAL.Words = ARGUMENTS.Text.Split(
- JavaCast( "string", " " )
- ) />
-
- <cfset LOCAL.Lines = [] />
-
- <cfset LOCAL.Lines[ 1 ] = {
- Text = "",
- Width = 0,
- Height = 0
- } />
-
-
- <cfloop
- index="LOCAL.WordIndex"
- from="1"
- to="#ArrayLen( LOCAL.Words )#"
- step="1">
-
- <cfset LOCAL.Word = LOCAL.Words[ LOCAL.WordIndex ] />
-
- <cfset LOCAL.Line = LOCAL.Lines[ ArrayLen( LOCAL.Lines ) ] />
-
-
- <cfset LOCAL.TextBounds = LOCAL.FontMetrics.GetStringBounds(
- JavaCast(
- "string",
- Trim( LOCAL.Line.Text & " " & LOCAL.Word )
- ),
- LOCAL.Graphics
- ) />
-
-
- <cfif (
- (LOCAL.TextBounds.GetWidth() LTE ARGUMENTS.Width) OR
- (NOT Len( LOCAL.Line.Text ))
- )>
-
- <cfset LOCAL.Line.Text &= (
- IIF(
- Len( LOCAL.Line.Text ),
- DE( " " ),
- DE( "" )
- ) &
- LOCAL.Word
- ) />
-
- <cfset LOCAL.Line.Width = Min(
- LOCAL.TextBounds.GetWidth(),
- ARGUMENTS.Width
- ) />
-
- <cfset LOCAL.Line.Height = LOCAL.TextBounds.GetHeight() />
-
- <cfelse>
-
-
- <cfset LOCAL.TextBounds = LOCAL.FontMetrics.GetStringBounds(
- JavaCast( "string", LOCAL.Word ),
- LOCAL.Graphics
- ) />
-
- <cfset LOCAL.Line = {
- Text = LOCAL.Word,
- Width = LOCAL.TextBounds.GetWidth(),
- Height = LOCAL.TextBounds.GetHeight()
- } />
-
- <cfset ArrayAppend(
- LOCAL.Lines,
- LOCAL.Line
- ) />
-
- </cfif>
-
- </cfloop>
-
-
-
-
- <cfloop
- index="LOCAL.LineIndex"
- from="1"
- to="#ArrayLen( LOCAL.Lines )#"
- step="1">
-
- <cfset LOCAL.Line = LOCAL.Lines[ LOCAL.LineIndex ] />
-
-
- <cfswitch expression="#ARGUMENTS.Attributes.TextAlign#">
-
- <cfcase value="right">
-
- <cfset LOCAL.X = (
- ARGUMENTS.X +
- ARGUMENTS.Width -
- LOCAL.Line.Width
- ) />
-
- </cfcase>
-
- <cfcase value="center">
-
- <cfset LOCAL.X = (
- ARGUMENTS.X +
- Fix(
- (ARGUMENTS.Width - LOCAL.Line.Width) /
- 2
- )) />
-
- </cfcase>
-
- <cfdefaultcase>
-
- <cfset LOCAL.X = ARGUMENTS.X />
-
- </cfdefaultcase>
- </cfswitch>
-
-
- <cfset LOCAL.Y = (
- ARGUMENTS.Y +
- (
- (LOCAL.LineIndex - 1) *
- ARGUMENTS.Attributes.LineHeight
- )) />
-
-
-
- <cfset ImageDrawText(
- ARGUMENTS.Source,
- LOCAL.Line.Text,
- LOCAL.X,
- LOCAL.Y,
- ARGUMENTS.Attributes
- ) />
-
- </cfloop>
-
-
- <cfreturn />
- </cffunction>
It's a lot of code, but it's not crazy complicated when you break it down piece by piece. The key to this whole algorithm is the ability to get the bounding dimensions (width and height) of a given string based the graphics "Font Metrics" in the context of the given Font.
On my ColdFusion 8 Test Server, I have set up a demo page (same link as above). If you are interested in the code that powers that page, here it is:
Launch code in new window » Download code as text file »
- <cfsilent>
-
- <cfparam
- name="FORM.font"
- type="string"
- default="courier new"
- />
-
- <cfparam
- name="FORM.size"
- type="numeric"
- default="24"
- />
-
- <cfparam
- name="FORM.text_align"
- type="string"
- default="right"
- />
-
- <cfparam
- name="FORM.line_height"
- type="numeric"
- default="#Fix(FORM.size * 1.5)#"
- />
-
- <cfparam
- name="FORM.text"
- type="string"
- default="I hope this doesn't come off as offensive, but seeing you in that dress will most definately be the highlight of my day."
- />
-
- <cfparam
- name="FORM.width"
- type="numeric"
- default="400"
- />
-
- </cfsilent>
-
- <cfoutput>
-
- <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
- <html>
- <head>
- <title>ImageDrawTextArea() ColdFusion 8 Demo</title>
-
- <style type="text/css">
-
- body {
- font-family: verdana ;
- font-size: 62.5% ;
- }
-
- p {
- clear: both ;
- font-size: 1.2em ;
- }
-
- label {
- float: left ;
- font-weight: bold ;
- width: 120px ;
- }
-
- input,
- select {
- width: 400px ;
- }
-
- </style>
- </head>
- <body>
-
- <h1>
- ImageDrawTextArea() ColdFusion 8 Demo
- </h1>
-
- <form action="#CGI.script_name#" method="post">
-
- <p>
- <label>Font:</label>
-
- <select name="font">
- <cfloop
- index="strFont"
- list="verdana,arial,courier new,georgia"
- delimiters=",">
-
- <option value="#strFont#"
- <cfif (FORM.font EQ strFont)>
- selected="true"
- </cfif>
- >#strFont#</option>
- </cfloop>
- </select>
- </p>
-
- <p>
- <label>Size:</label>
-
- <select name="size">
- <cfloop
- index="intSize"
- from="8"
- to="40"
- step="1">
-
- <option value="#intSize#"
- <cfif (FORM.size EQ intSize)>
- selected="true"
- </cfif>
- >#intSize#</option>
- </cfloop>
- </select>
- </p>
-
- <p>
- <label>Align:</label>
-
- <select name="text_align">
- <cfloop
- index="strAlign"
- list="left,center,right"
- delimiters=",">
-
- <option value="#strAlign#"
- <cfif (FORM.text_align EQ strAlign)>
- selected="true"
- </cfif>
- >#strAlign#</option>
- </cfloop>
- </select>
- </p>
-
- <p>
- <label>Line Height:</label>
-
- <select name="line_height">
- <cfloop
- index="intHeight"
- from="10"
- to="50"
- step="1">
-
- <option value="#intHeight#"
- <cfif (FORM.line_height EQ intHeight)>
- selected="true"
- </cfif>
- >#intHeight#</option>
- </cfloop>
- </select>
- </p>
-
- <p>
- <label>Text:</label>
-
- <input type="text" name="text" value="#HtmlEditFormat( FORM.text )#" />
- </p>
-
- <p>
- <label>Textarea Width:</label>
-
- <select name="width">
- <cfloop
- index="intWidth"
- list="100,200,300,400,500"
- delimiters=",">
-
- <option value="#intWidth#"
- <cfif (FORM.width EQ intWidth)>
- selected="true"
- </cfif>
- >#intWidth#</option>
- </cfloop>
- </select>
- </p>
-
- <p>
- <button type="submit">
- Generate Image »
- </button>
- </p>
-
- </form>
-
-
- <h2>
- Rendered Image and Text Area
- </h2>
-
-
- <cfset imgCanvas = ImageNew( "", 545, 350, "rgb" ) />
-
- <cfset objAttributes = {
- Font = FORM.font,
- Size = FORM.size,
- TextAlign = FORM.text_align,
- LineHeight = FORM.line_height
- } />
-
- <cfset ImageSetAntialiasing(
- imgCanvas,
- "on"
- ) />
-
- <cfset ImageDrawTextArea(
- imgCanvas,
- FORM.text,
- 50,
- 50,
- FORM.width,
- objAttributes
- ) />
-
- <cfimage
- action="writetobrowser"
- source="#imgCanvas#"
- />
-
-
- </body>
- </html>
-
- </cfoutput>
Hopefully, this will make drawing text on a ColdFusion 8 image a whole lot easier.
Download Code Snippet ZIP File
Comments (7) |
Post Comment |
Ask Ben |
Permalink |
Print Page
Wow, I love it.
Posted by Ryan Stille
on Sep 29, 2007
at 2:24 PM
@Ryan,
Thanks. It was a lot of fun to write.
Posted by Ben Nadel
on Sep 29, 2007
at 6:12 PM
Excellent work, Ben, but I'd have to say I think you're thinking about this problem in the wrong way because of the difference between textarea and CFIMAGE. You can't make a UDF to wrap text onto a CFIMAGE in the same way a textarea does because a CFIMAGE doesn't have a scrollbar. If you enter 100 for the width on your demo and leave the default text there, you can't see most of the string because it goes below the image area.
Your solution will work for some situations, but you still have to do the work of picking the right font size for the area available. This will be hunky-dory in some situations with mainly static content but as soon as you have dynamic text, you may run into the problem with text vanishing outside the bounds of the CFIMAGE.
I'm working on a UDF where you pass a string, and have the function scale the font to fit inside the CFIMAGE area. I started this a while back and nearly got it working but ran out of time. I posted the code in a comment on Ray's blog, in a CFIMAGE related entry on 17/9 (I only remember that because it's the day before my birthday :p). I can't remember how much of it I got working but if it's of interest, go have a look. I might have some time this afternoon to actually finish it off!
Posted by George Bridgeman
on Sep 30, 2007
at 7:10 AM
@George,
Interesting. I never thought of auto-scaling. It would be an interesting problem. You would have to have some conditional loop. See, you can't just figure out the font size once because each time to you adjust the font size, it will adjust the spacing between lines as well as the number of lines that need to be rendered. So, you'd have to kind of loop OVER the meat of my algorithm until the entire set of dimensions lines up nicely.
I will have to take a look at what you posted on Ray's blog.
Posted by Ben Nadel
on Sep 30, 2007
at 1:20 PM
Oh, that´s great. This is what i need. Cause i search exatly this code for my own little project. Thanks for share it! Very usefully
Posted by Webdesign Agentur
on Dec 29, 2007
at 1:52 PM
Very interesting article. I try to learn ColdFusion 8, because i think it would be nice for my next project to work with ColdFusion 8.... And now i search many informations about ColdFusion. Thanks for it ;)
Posted by Werbeagentur
on Jan 23, 2008
at 7:29 AM
Great tips.
Posted by ana
on May 6, 2008
at 3:30 PM
Post Comment |
Ask Ben