Introduction
A recent project had to display long labels in the header of a table with a lot of columns. While the columns' content kept small, the header was growing huge. The best way to accomplish the task would have been to use 900 rotated texts. However, I did not know any techniques for rotating texts in HTML. So - here is the idea of another WebControl for creating a bitmap on the fly and rendering it in the page to simulate a vertical label.
During developing this project, I've encountered many articles dealing with creating images on the fly. However, lots of small details came in the way and I feel that summarizing them in an article would be beneficial.
Prerequisites
The only choice I had was to - somehow - build small bitmaps and use them in <IMG SRC=...>
tags. Then, create a WebControl for simplifying the task of building pages. A WebControl would have an additional benefit: it could bind its Text
property so the page would dynamically create labels or localize them.
However, I do not want to store all generated images in files on the hard drive: each file would have a unique name and, beside the problem with generating (long) names, files will soon accumulate in large quantities on the server and somehow somebody should manage or delete them. Fortunately, the IMG SRC
tag attribute accepts a URL as the file name, and this URL can be as well the URL of a program/script/page which returns a stream in the same way the bitmap of GIF/JPG file would.
So, the task splits in two smaller tasks:
- Create a generator page for the content needed in the
SRC
attribute,
- Wrap all that in a WebControl.
VerticalText page
Let's start coding the VerticalText.aspx page:
namespace WebImageTest {
public class VerticalText : System.Web.UI.Page {
private void Page_Load(object sender, System.EventArgs e) {
Response.ContentType = "image/png";
Response.BinaryWrite( ms.ToArray() );
}
}
}
This ASPX is peculiar in the sense of its content: instead of returning HTML content (<HEADER>
, <BODY>
etc.), it returns a picture. This is achieved in the Page_Load
method, with the line:
Response.ContentType = "image/png";
ContentType
, with the default "text/HTML"
can change the response in various ways and produce interesting results. For example, you may start Excel on the client machine (assuming that the client machine has Excel installed.) In the same way, with a content of "image/bmp"
, you may start directly Paintbrush
or with "image/gif"
- PhotoPaint (assuming the corresponding file-extension associations).
The above code generates PNG (Portable Network Graphic) content.
We target that, when typing the following address http:
, the browser would render the following image:
It is worth mentioning here why I had to choose the PNG format over all other formats available. First of all, with GIF, I would have been able to use the following (shorter/fewer) lines:
Response.ContentType = "image/gif";
bm.Save(Response.OutputStream, ImageFormat.Gif);
However, the output generated this way is raster-ed by the usage of a palette of 256 colors. Saving in the OutputStream
works as well for the JPEG format, but the JPEG image is a bit dirty due to an uncontrollable compression rate. On the other hand, the PNG format looks well on the screen, but cannot be saved in the OutputStream
(albeit there is no hint why the trick with creating a MemoryStream
works!)
Let's throw some parameters
Of course, we need a parameter for the text of the label. Then it is as well important to be able to specify the font, unless we want to stick with the defaults. Then, we may extend a bit the idea by being able to change the background color and the ink of the text. A lithe padding would be beneficial in certain situations.
Defaults are good: when users forget or don't need to give values, the object should be able to render something instead of giving the x box. Page_Load
starts by getting these values from the request URL. By design, the URL should look like this:
VerticalText.aspx?Text=Hello World&Font=
Arial|24|B&BgColor=DarkGoldenrod&FrColor=Orange&Padding=3
As everybody knows, after the name of the page name, parameters are introduced with ?
and are separated with &
. To simplify and reduce the length of the line, I have compacted the font parameters using the |
separator and ruling an order as: <Font name>|<Font Size>|<Font attributes>
. In fact, the code:
string fontName = "Arial";
int fontSize = 12;
FontStyle fontStyle = new FontStyle();
try {
string [] fontStr =
Request["Font"].Split(new char[] {'|',',',' ','/',':',';'});
try { if (fontStr[0]!=string.Empty) fontName = fontStr[0]; } catch { }
try { fontSize = int.Parse(fontStr[1]); } catch { }
try {
if (fontStr[2].IndexOf('B')>=0) fontStyle |= FontStyle.Bold;
if (fontStr[2].IndexOf('I')>=0) fontStyle |= FontStyle.Italic;
if (fontStr[2].IndexOf('U')>=0) fontStyle |= FontStyle.Underline;
} catch {}
} catch {}
Font font = new Font(fontName, fontSize, fontStyle);
allows a lot more delimiters (including blank) and prepares the defaults. The try
...catch
blocks are there just to make sure that defaults will stay when the request does not have requested elements.
Do the same for bgColor
, frColor
and pad
. Problems with colors: first of all, I want my users to be able to use named colors as Red
, or DarkGoldenrod
or ControlDark
. In the same time, I want to be able to choose any unnamed color like #804040
. Well, there is a glitch that I could not fix: I can not use the #
char in the URL. So, I decided to change it in $
. The private method StrToColor
will do the rest, and I believe is clear enough:
private Color StrToColor(string c) {
if (c.Trim().IndexOf("$")==0)
return ColorTranslator.FromHtml(c.Replace("$","#"));
else
return Color.FromName(c);
}
Then set some defaults and decode the colors:
int pad = 0;
Color bgColor = Color.White;
Brush br = Brushes.Black;
try { pad = int.Parse(Request["Padding"]); } catch { }
try { bgColor = StrToColor(Request["BgColor"]); } catch { }
try { br = new SolidBrush( StrToColor(Request["FrColor"]) ); } catch { }
From a different angle
There are two ways to measure the size of the bitmap: measure the string rendered horizontally and reverse the width
with the height
, or use StringFormatFlags.DirectionVertical
. (Thanks to Chris Garrett for this trick!):
StringFormat format = new StringFormat(StringFormat.GenericDefault);
format.FormatFlags = StringFormatFlags.MeasureTrailingSpaces |
StringFormatFlags.DirectionVertical;
SizeF sz = (Graphics.FromImage(new Bitmap(1,1))).MeasureString(Text,
font, Point.Empty, format);
Bitmap bm = new Bitmap((int)sz.Width+2*pad,(int)sz.Height+2*pad);
Drawing the string this way, however, brings a perceptive problem: the string starts with the first character on the top while the bottom of the text is oriented towards the left. As I have the European school, I know that to be easy to read a vertical text, the string should be oriented the other way. There are pros and cons for that, and I'm going in to details. Firstly, at technical drawing lessons, I've learned that one should take a blueprint with the right hand from the upper-right corner and with the left hand from the lower-right corner. Then drag the paper from vertical to horizontal and the vertical text should become horizontal in the natural way. Americans however, do the other way (like all the other ways to figure how to do a projection in drawing!) and same does the Microsoft flag for rendering vertical texts. Personally, I feel that, in front of a fixed screen, it's easier to turn the head to the left instead of the right to read a vertical text (it's so natural that you do not have even to turn the head). Observe that Excel also adheres to this rule when it rotates texts in cells.
We can also argue about how multiple lines vertical text should be rendered, but for a text box, it makes no difference - can go both ways. However, for my project, I have to put vertical labels in the header of a table and we read table columns (as normal text) from left to right:
Tip: to make the vertical text even more readable, make it Italic
.
So, the bottom line is that we have to rotate the text again:
System.Drawing.Graphics g = System.Drawing.Graphics.FromImage(bm);
try {
g.Clear(bgColor);
g.TextRenderingHint = TextRenderingHint.ClearTypeGridFit;
g.TranslateTransform ((int)sz.Width,(int)sz.Height);
g.RotateTransform(180.0F);
g.DrawString(Text, font, br, -1-pad, 1-pad, format);
Now, push the bitmap content in the Response
and clean up the resources:
MemoryStream ms = new MemoryStream();
bm.Save(ms,ImageFormat.Png);
Response.ContentType = "image/png";
Response.BinaryWrite( ms.ToArray() );
ms.Close();
} finally {
g.Dispose();
bm.Dispose();
font.Dispose();
}
Peculiar use
To refer the VerticalText.aspx page directly from the address bar of the browser makes not much sense - it will render just a label per screen.
We target, however, to use this reference in the SRC=
attribute of images (<Img
tag) or image buttons. For example, using in a form the following code:
<asp:ImageButton id="ImageButton1" runat="server"
ImageUrl="VerticalText.aspx?Text= Login &Font=|16|B&BgColor=Silver&FrColor=Window"
BorderStyle="Outset" BorderWidth="1px" AlternateText="Login">
</asp:ImageButton>
we may get ourselves a beautiful vertical button:
Now, pack it up!
The second task on our list was to pack all that in an easy to use WebControl - the VerticalLabel
. The control will have to manage with its properties the text of the label, the font, and the colors. It will be also responsible for rendering the HTML code that uses the above VerticalText.aspx page. It would be nice to have the control on the ToolBox
with a nice glyph to represent it there. (I have already discussed what the challenges for creating this small bitmap are and how to put it on the ToolBox
, in my previous article Numeric Text Box.)
So, inherit the VerticalLabel
control directly from the WebControl
and add a reference to the INamingContainer
interface, so the control will get a unique ID
. Accept as it is the auto generated Text
property with its Bindable(true)
attribute. Observe also that the control has by default a Font
and a BackColor
. What we miss is the Padding
property that we add ourselves.
Next, we just have to generate the HTML code in the Render
method:
protected override void Render(HtmlTextWriter output) {
StringBuilder s = new StringBuilder("<img ID="); s.Append(this.ID);
s.Append(" src=\"VerticalText.aspx?Text="); s.Append(Text);
s.Append("&Font="); s.Append(Font.Name); s.Append("|");
Using a StringBuilder
seems to be a good idea. (Although we could write directly in the output
. However, this way may help when debugging.) Keep adding items separately instead of concatenating them with the +
string operator. Let's have the Font
parameter added anyway, whether its name is empty or not - the VerticalText.aspx page will have a default for that. In the same way, add the size
and the font
attributes by the rules VerticalText.aspx page accepts:
if (Font.Size.Unit.Value>0)
s.Append(Font.Size.Unit.Value.ToString());
if (Font.Bold || Font.Italic || Font.Underline) {
s.Append("|");
if (Font.Bold ) s.Append("B");
if (Font.Italic ) s.Append("I");
if (Font.Underline) s.Append("U");
}
Don't add the BackColor
and ForeColor
when they had no value, but use a WebColorConverter
if it was a known color, or the ColorTranslator
to convert ToHtml
otherwise. Remember, we were not able to use the #
char in the URL, so replace it with $
.
if (!BackColor.IsEmpty) {
s.Append("&BgColor=");
s.Append(((BackColor.IsKnownColor)
?(new WebColorConverter()).ConvertToString(BackColor)
:ColorTranslator.ToHtml(BackColor).Replace("#","$")));
}
if (!ForeColor.IsEmpty) {
s.Append("&FrColor=");
s.Append(((ForeColor.IsKnownColor)
?(new WebColorConverter()).ConvertToString(ForeColor)
:ColorTranslator.ToHtml(ForeColor).Replace("#","$")));
}
if (Padding>0) {
s.Append("&Padding=");
s.Append(Padding.ToString());
}
s.Append("\"");
Just to have a horizontal text if something goes wrong in the VerticalText.aspx page, add the ALT=
attribute to the HTML component. This might be a little odd when everything goes well, since the Alt text becomes for the label a hint with exactly the same text; so, you may decide to keep or discard the next line:
s.Append(" Alt=\""); s.Append(Text); s.Append("\"");
Close the tag and write all that to the output
. When debugging, you may place a breakpoint on the output statement or anywhere above to see how the string accumulates.
s.Append(">");
output.Write(s.ToString());
}
Wish List
I wish I was able to include the VerticalText.aspx page in the assembly of the VerticalLabel
control. It seems that this is not possible - the page should be part of the final assembly or be a separate one. (Then the above code should include the full path to that assembly, the control should include a property to point to that assembly URL, and the user would have to type that URL seed for each vertical label in the project; considering all that, it seems that the price to copy the VerticalText.aspx page in the final assembly is not too high.)
When I've finished writing this project, an article from asp.netPRO magazine (September 2004) - "Custom HTTP Handlers" by Dino Exposito - was suggesting that it would have been possible to use an IHttpHandler
interfaced class to achieve the same effect as with the VerticalText.aspx page. I might go in that soon, but again - the price to pay for reconfiguring the IIS and having the handler active on any other project from the server seems to be as well too high at this point. Maybe with some added functionality as variable angles, graphic effects, and retrieving background/watermark images from databases or other sources...
I wish also being able to include characters like &
and #
in the caption of the label or in the specification of unknown colors.
The technique described here does not support transparent bitmaps. There is an excellent article - "Transparent GIFs with GDI+ / System.Drawing" about re-coloring GIFs and making them transparent, but the code is a bit too stuffy, and I'm not going yet to include it in here; in fact, my project works perfectly without...
Maybe, I'll get some good suggestions from you...