Introduction
Ever felt frustrated by the range of fonts available in HTML/CSS? Though it's a limitation we've all adapted to, there are occasions when it would be nice to have a big header rendered in a font that sits nicely with your design or the font used in a logo. I've seen it done with Flash, but that's like killing a fly with a bazooka, and has the problem of being restricted to Flash-enabled browsers (what about iPhone users, for example?). Then, there's the option of creating the heading in Photoshop and inserting the image, but that is time-consuming, and impossible for a content-managed site.
That was the problem I confronted while working on the site you see a snippet of above. I wanted the text of the header to match that used in the logo and other images, and I needed it to work in with the Content Management System I'd devised to run the site, so the page headers would display in the correct font without the web administrator even being aware that anything unusual was going on. What I wanted, in fact, was a user control where you just plug in some parameters like the text to display, font, size, color etc., and the text gets rendered to the page as an image. That's exactly what this code does.
How it works
Ever wondered how a captcha image works? How do you get some arbitrary piece of text and display it as an image? The basic idea (in .NET) is to use GDI to render the text as a GIF and then embed an <img>
tag pointing to it. If you know a little GDI, that's not so hard. But, this particular problem has some unique challenges:
- A captcha image which gets rendered once on a few forms is one thing, but what about server load if you're rendering headings on every page in your site?
- What about long strings? We need the control to be able to wrap text.
- What about the search engine implications of replacing text with images? Headings provide vital information about a web-page that might get lost if we substitute an image.
- GDI font rendering does not natively support control over kerning and spacing. I needed to be able to control these font display elements in order to get my Rockwell headers to match the Rockwell font used elsewhere in the design.
How did I solve these problems? Firstly, I managed the server load issue by ensuring that each header only gets rendered once. When the user control receives a request, it generates a unique file name based on the parameters passed to it. Every time it receives a request, it checks if a file by this name already exists before it renders the text, thus making sure that it only does the rendering work once. (In retrospect, I think this feature may be overkill. The likely load of using GDI is probably very small, even in a high-traffic scenario, but it does at least solve the issue of how we handle getting our images to the page. If we'd opted to return the image directly in the way captcha type programs work, it would mean passing our parameters through another implementation layer.)
Line wrapping was the biggest complication, especially since my need to control kerning meant that I had to render each character individually. Most of the code complications arise from having to measure the size required to render each string and then wrap at the appropriate point.
To solve the search engine problem, in addition to displaying the image of the text, the code outputs the header text in standard HTML tags (you provide the type of tag as a parameter), with a CSS class that you can set up in your style sheet to display:none
. Thus, bots get to see the text, but your visitors won't.
Using the code
Using the user control could hardly be easier. Just add the control to your web project and reference it as per the example page (Example.aspx). The parameters you can use are as follows:
Text
: The text of the header you want to render.FontName
: The name of the font you want to use. Obviously, it has to be installed on the server, and it has to be supported by GDI (i.e., no OpenType, sadly).FontStyle
: A FontStyle
value (bold, italic etc.).TextColor
: A color value in the format "#rrggbb". Note, some valid CSS codes will fail: the string must have six digits.BgColor
: The background color.MaxWidth
: The maximum width in pixels allowed for the text before it wraps.CssClass
: A CSS class to attach to the image tag in the HTML - this allows you to set parameters like margins.LineHeightAdjustment
: A pixel value (float) to add to the default space between lines. Negative values are possible.CharWidthAdjustment
: A pixel value (float) to add to the default space between characters (kerning).SpaceAdjustment
: A pixel value (float) to add to the default space between words.OutputHeaderTags
: Set to true if you want to output the text in HTML tags as well as as an image.TagType
: The type of tag you want to wrap your text in (h1, h2 etc.). Don't add the angled braces.TextCssClass
: The CSS class to add to the header tag. This should be set to display:none
in your style sheet.
Gotchas
There are only a couple of traps. Firstly, you need to define a class in your stylesheet with display:none
and set that as the CssClass
parameter when you use the user control. Yes, it could simply have output "style="display:none"
", but I figured this might annoy bots on the lookout for dodgy practices. I don't know if my method is actually any better though... A little JavaScript doovery to hide the text might be the best bet if you're really concerned about it.
The other thing is I have used an AppSetting called "ImagePath
" to set the path to where the rendered images should be saved. So, you'll need to create that, or else modify the code appropriately.
The code
using System;
using System.Collections;
using System.Configuration;
using System.Data;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Text;
using System.Text.RegularExpressions;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Drawing.Text;
using System.Collections.Generic;
using System.IO;
public partial class RenderHeader : System.Web.UI.UserControl
{
private string _text = "";
private int _spaceAdjustment = 5;
public int SpaceAdjustment
{
get { return _spaceAdjustment; }
set { _spaceAdjustment = value; }
}
private int _lineHeightAdjustment = 0;
public int LineHeightAdjustment
{
get { return _lineHeightAdjustment; }
set { _lineHeightAdjustment = value; }
}
private float _charWidthAdjustment = 0;
public float CharWidthAdjustment
{
get { return _charWidthAdjustment; }
set { _charWidthAdjustment = value; }
}
public string Text
{
get { return _text; }
set { _text = value; }
}
private string _fontName = "Arial";
public string FontName
{
get { return _fontName; }
set { _fontName = value; }
}
private FontStyle _fontStyle = FontStyle.Regular;
public FontStyle FontStyle
{
get { return _fontStyle; }
set { _fontStyle = value; }
}
private float _fontSize = 12.0F;
public float FontSize
{
get { return _fontSize; }
set { _fontSize = value; }
}
private string _textColor = "#000000";
public string TextColor
{
get { return _textColor; }
set { _textColor = value; }
}
private string _bgColor = "#ffffff";
public string BgColor
{
get { return _bgColor; }
set { _bgColor = value; }
}
private int _maxWidth = 3000;
public int MaxWidth
{
get { return _maxWidth; }
set { _maxWidth = value; }
}
private string _cssclass = "";
public string CssClass
{
get { return _cssclass; }
set { _cssclass = value; }
}
private bool _outputHeaderTags = true;
public bool OutputHeaderTags
{
get { return _outputHeaderTags; }
set { _outputHeaderTags = value; }
}
private string _tagType ="h1";
public string TagType
{
get { return _tagType; }
set { _tagType = value; }
}
private string _textCssClass = "";
public string TextCssClass
{
get { return _textCssClass; }
set { _textCssClass = value; }
}
private string _fileName
{
get
{
string s = _text + _fontSize + _fontName + _bgColor.Substring(1) +
_textColor.Substring(1) + _charWidthAdjustment.ToString() +
_fontStyle.ToString() + _maxWidth.ToString() +
_lineHeightAdjustment.ToString() + _spaceAdjustment.ToString() + ".gif";
s = MakeValidFileName(s);
return s;
}
}
private string _filePath
{
get
{
return Server.MapPath(
ConfigurationManager.AppSettings["ImagePath"]) + @"\" + _fileName;
}
}
public RenderHeader()
{
}
protected void Page_Load(object sender, EventArgs e)
{
if (!File.Exists(_filePath))
{
GenerateImage();
}
System.Web.UI.WebControls.Image HeaderImage = new System.Web.UI.WebControls.Image();
HeaderImage.ImageUrl=
ConfigurationManager.AppSettings["ImagePath"] + "/" + _fileName;
HeaderImage.AlternateText = _text;
HeaderImage.CssClass = _cssclass;
this.Controls.Add((Control)HeaderImage);
if (_outputHeaderTags)
{
System.Web.UI.LiteralControl header = new LiteralControl();
header.Text = "<" + _tagType + " class='" +
_textCssClass + "'>" + _text +
"</" + _tagType + ">";
this.Controls.Add(header);
}
}
private void GenerateImage()
{
Bitmap bitmap = new Bitmap(1, 1, PixelFormat.Format32bppArgb);
Graphics g = Graphics.FromImage(bitmap);
StringFormat format = StringFormat.GenericTypographic;
Font font = new Font(_fontName, _fontSize, _fontStyle);
SizeF size = g.MeasureString(_text, font);
float lineHeight = size.Height;
List<string> lines = new List<string>();
float widestLine = 0F;
string[] words = _text.Split(' ');
StringBuilder sbLine = new StringBuilder();
int numSpaces = 0;
float lineWidth = 0F;
for(int i=0; i<words.Length; i++)
{
string w = words[i];
string line = sbLine.ToString();
if (line.Length > 0)
{
sbLine.Append(" ");
numSpaces += 1;
}
sbLine.Append(w);
string testLine = sbLine.ToString();
for (int i2 = line.Length; i2 < testLine.Length; i2++)
{
SizeF sf = g.MeasureString(testLine[i2].ToString(),
font, new PointF(lineWidth, 0F), format);
if (testLine[i2] == ' ')
{
lineWidth += (float)(sf.Width) + _spaceAdjustment;
}
else
{
lineWidth += (float)(sf.Width + _charWidthAdjustment);
}
}
if (lineWidth > (float)_maxWidth)
{
lines.Add(line);
size.Height += lineHeight +_lineHeightAdjustment;
lineWidth = 0;
sbLine.Length = 0;
numSpaces = 0;
sbLine.Append(w);
}
else
{
if (lineWidth > widestLine)
{
widestLine = lineWidth;
}
}
if (i == words.Length-1)
{
lines.Add(sbLine.ToString());
}
}
int width = (int)widestLine +2;
int height= (int)size.Height +1;
bitmap = new Bitmap(width, height, PixelFormat.Format32bppArgb);
g = Graphics.FromImage(bitmap);
g.SmoothingMode = SmoothingMode.HighQuality;
g.TextRenderingHint= TextRenderingHint.ClearTypeGridFit;
SolidBrush sb = new SolidBrush(ColorTranslator.FromHtml(BgColor));
Rectangle rect = new Rectangle(0, 0, width, height);
g.FillRectangle(sb, rect);
sb = new SolidBrush(ColorTranslator.FromHtml(TextColor));
float ypos = 0F;
foreach (string l in lines)
{
float xpos = 0F;
for (int i = 0; i < l.Length; i++)
{
g.DrawString(l.Substring(i, 1), font, sb,
new PointF(xpos, ypos),format);
SizeF sf = g.MeasureString(l.Substring(i, 1),
font, new PointF(xpos, ypos), format);
if (l.Substring(i,1) == " ")
{
xpos += (float)(sf.Width) + _spaceAdjustment;
}
else
{
xpos += (float)(sf.Width + _charWidthAdjustment);
}
}
ypos += (lineHeight + _lineHeightAdjustment);
}
bitmap.Save(_filePath, ImageFormat.Gif);
bitmap.Dispose();
font.Dispose();
g.Dispose();
sb.Dispose();
}
private static string MakeValidFileName(string name)
{
string invalidChars = Regex.Escape(new string(Path.GetInvalidFileNameChars()));
string invalidReStr = string.Format(@"[{0}]", invalidChars);
return Regex.Replace(name, invalidReStr, "_");
}
}
Comments and reflections
I've rather thoroughly commented this code in the hope you'll follow the fairly elaborate logic. Everything would be a lot simpler if we didn't need the kerning adjustment. The process of writing the code was an interesting insight for me into the slightly lower-level ins and outs of text rendering. After all, precisely these same types of algorithms must be employed all the time in browsers, word processors - in fact, any app that has to display and wrap text.
There's a fair bit of scope to add more features to this control. You could add fancy stuff like drop shadows if you felt like getting your hands dirty with GDI. I nearly did, but then I remembered I was supposed to be working... :) Another nice addition would be support for centered text.
You could potentially also render larger slabs of text with this control, but I wouldn't recommend it. Users can't select text in images, and of course, it's impossible to resize the window and have the text readjust - once it's rendered, it's rendered.
Well, that's about it! I hope you enjoy using this little gizmo in your web apps to add some pretty headings to your web pages. To see it in action, take a look at www.menslineaus.org.au.
Pierz works as web development manager for a not-for-profit organization in Melbourne, Australia. In his spare time, he writes fiction.