Dynamic Heading Generator
There is a PHP script on A List Apart for dynamically generating images for paragraph headings using fonts. I have successfully translated this script into C#.
The Dynamic Text Replacement article by Stewart Rosenberger provides a PHP script that uses the GD graphics library to create images using the specified font and text. The article claims "Although we used PHP to construct the images in this implementation, your website does not need to be actively using PHP to take advantage of this technique." but I doubt that anyone has bothered to provide another implementation of this technique. Fortunately, there is a .NET wrapper for the GD Library known as GD-Sharp so it is possible to use this on an ASP.NET powered site.
This is very useful if your web hosting company does not have the GD Library enabled in their php.ini file. My web site supports PHP scripting and the GD Library but I was unable to get this script to work there. Fortunately, my ASP.NET version provides an alternative and I was able to get it working on my web site at: http://www.williamsportwebdeveloper.com/heading.ashx?text=ASP.NET%20WebLog. If you come across any other interesting PHP scripts that rely upon the GD Library to generate images you should be able to use GD-Sharp to do something similar using ASP.NET. For example, there are PHP scripts to generate captcha images this way. I'll probably convert one to C# in a later blog post.
To use this on your ASP.NET site you must import bgd.dll, GD-Import.dll, and GD-Sharp.dll into your bin directory. Since I used a trace listener to log errors to a text file you will also need to add a section to your web.config file:
1: <system.codedom>
2: <compilers>
3: <compiler language="c#;cs;csharp" extension=".cs"
4: compilerOptions="/d:TRACE"
5: type="Microsoft.CSharp.CSharpCodeProvider, System, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
6: warningLevel="1"/>
7: <compiler language="VB" extension=".vb"
8: compilerOptions="/d:Trace=true"
9: type="Microsoft.VisualBasic.VBCodeProvider, System, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"/>
10: </compilers>
11: </system.codedom>
Then you need to create directories for the image cache and the trace file and give those directories write permission for the ASP.NET user account. It was difficult to translate the PHP code into C# and I was unable to find a good alternative for the ImageTTFBBox method so forgive the ugly hack:
1: <%@ WebHandler Language="C#" Class="heading" %>
2: /*
3: Dynamic Heading Generator
4: By Stewart Rosenberger
5: http://www.stewartspeak.com/headings/
6: *
7: * C#.NET conversion by Robert S. Robbins
8:
9: This script generates PNG images of text, written in
10: the font/size that you specify. These PNG images are passed
11: back to the browser. Optionally, they can be cached for later use.
12: If a cached image is found, a new image will not be generated,
13: and the existing copy will be sent to the browser.
14:
15: Additional documentation on PHP's image handling capabilities can
16: be found at http://www.php.net/image/
17: *
18: * Additional documentation on the GD-Sharp .NET wrapper for the GD Library can
19: * be found at http://gd-sharp.sourceforge.net/
20: */
21: using System;
22: using System.Web;
23: using System.Diagnostics;
24: using System.Runtime.InteropServices;
25: using System.IO;
26: using System.Drawing;
27: using Ntx.GD;
28: using System.Collections;
29: using System.Security.Cryptography;
30: using System.Text;
31:
32: public class heading : IHttpHandler {
33:
34: private string font_file = @"C:\WINDOWS\Fonts\comic.ttf";
35: private string font_name = "Comic Sans MS";
36: private int font_size = 30;
37: private string font_color = "#000000";
38: private string background_color = "#ffffff";
39: private bool transparent_background = true;
40: private bool cache_images = true;
41: private string cache_folder = @"D:\inetpub\williamsportwebdeveloper\cache";
42:
43: /*
44: ---------------------------------------------------------------------------
45: For basic usage, you should not need to edit anything below this comment.
46: If you need to further customize this script's abilities, make sure you
47: are familiar with PHP and C#.NET and its image handling capabilities.
48: ---------------------------------------------------------------------------
49: */
50:
51: private string mime_type = "image/png";
52: private string extension = ".png";
53: private int send_buffer_size = 4096;
54:
55: public void ProcessRequest (HttpContext context) {
56: // create trace listener file for debugging purposes
57: System.IO.Stream objFile = System.IO.File.Create(@"D:\inetpub\williamsportwebdeveloper\app_data\trace.txt");
58: TextWriterTraceListener objTextListener = new TextWriterTraceListener(objFile);
59: Trace.Listeners.Add(objTextListener);
60: Trace.AutoFlush = true;
61:
62: try
63: {
64: // This is the equivalent of calling ImageCreate
65: GD img = new GD(1, 1, true);
66:
67: if (HttpContext.Current.Request.QueryString["text"] == null)
68: {
69: Trace.WriteLine("Fatal Error: No text specified.");
70: }
71:
72: // clean up text
73: string text = HttpContext.Current.Request.QueryString["text"];
74: text = text.Replace(@"\", "");
75:
76: // look for cached copy, send if it exists
77: string hash = GenerateMD5Hash(font_name, font_size.ToString(), font_color, background_color, transparent_background.ToString(), text);
78: string cache_filename = cache_folder + @"\" + hash + extension;
79: if (cache_images)
80: {
81: // check image file availability
82: if (File.Exists(cache_filename))
83: {
84: Trace.WriteLine("Serving image file: " + cache_filename + " from the cache");
85: context.Response.ContentType = mime_type;
86:
87: FileStream fs = File.OpenRead(cache_filename);
88: byte[] buffer = ReadFully(fs);
89: context.Response.BinaryWrite(buffer);
90: context.Response.Flush();
91:
92: // exit immediately
93: Trace.Close();
94: Trace.Flush();
95: return;
96: }
97: }
98:
99: // check font availability
100: if (File.Exists(font_file) == false)
101: {
102: Trace.WriteLine("Fatal Error: The server is missing the specified font.");
103: }
104:
105: // create image
106: int dip = get_dip(font_name, font_size);
107: int[] box = ImageTTFBBox(font_name, font_size, text);
108: // This is the equivalent of calling ImageCreate
109: GD image = new GD(box[0], box[1], true);
110: if (image == null)
111: {
112: Trace.WriteLine("Fatal Error: The server could not create this heading image.");
113: }
114:
115: // allocate colors and draw text
116: Color font_rgb = Color.FromArgb(ColorTranslator.FromHtml(font_color).ToArgb());
117: GDColor fg = image.ColorAllocate(font_rgb.R, font_rgb.G, font_rgb.B);
118: Color background_rgb = Color.FromArgb(ColorTranslator.FromHtml(background_color).ToArgb());
119: GDColor bg = image.ColorAllocate(background_rgb.R, background_rgb.G, background_rgb.B);
120: image.FilledRectangle(0, 0, box[0], box[0], bg);
121: // bounding rectangle
122: ArrayList br = new ArrayList();
123: br.Add(new Ntx.GD.Point(0, 0));
124: br.Add(new Ntx.GD.Point(box[0], 0));
125: br.Add(new Ntx.GD.Point(box[0], box[1]));
126: br.Add(new Ntx.GD.Point(0, box[1]));
127: // This is the equivalent of calling ImageTTFText
128: string result = image.StringFT(br, fg, font_file, font_size, 0, 0, font_size, text, true);
129: Trace.WriteLine(result, "result");
130:
131: // set transparency
132: if (transparent_background)
133: {
134: image.ColorTransparent(bg);
135: }
136:
137: // save copy of image for cache
138: if(cache_images)
139: {
140: image.Save(GD.FileType.Png, cache_filename, 1);
141: }
142:
143: context.Response.ContentType = mime_type;
144: MemoryStream ms = new MemoryStream();
145: image.Save(GD.FileType.Png, ms);
146: byte[] binary = ms.ToArray();
147: context.Response.BinaryWrite(binary);
148: context.Response.Flush();
149: }
150: catch (Exception ex)
151: {
152: Trace.WriteLine("Fatal Error: Server does not support PHP image generation");
153: Trace.WriteLine(ex.ToString());
154: }
155: finally
156: {
157: Trace.Close();
158: Trace.Flush();
159: }
160: }
161:
162: /// <summary>
163: /// Try to determine the "dip" (pixels dropped below baseline) of this
164: /// font for this size.
165: /// </summary>
166: /// <param name="font">The name of a TTF font.</param>
167: /// <param name="size">The size of the font.</param>
168: /// <returns>CellDescent, the height below base line.</returns>
169: /// <remarks>Not exactly the same as the original function, but the best we can do.</remarks>
170: public int get_dip(string font, int size)
171: {
172: // Create the Font object for the font at that size
173: System.Drawing.Font objFont = new System.Drawing.Font(font, size, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Pixel);
174:
175: // Determine the CellDescent
176: int intCellDescent = size * objFont.FontFamily.GetCellDescent(objFont.Style) / objFont.FontFamily.GetEmHeight(objFont.Style);
177: Trace.WriteLine(size.ToString(), "size");
178: Trace.WriteLine(objFont.FontFamily.GetCellDescent(objFont.Style).ToString(), "CellDescent");
179: Trace.WriteLine(objFont.FontFamily.GetEmHeight(objFont.Style).ToString(), "GetEmHeight");
180: Trace.WriteLine(intCellDescent.ToString(), "intCellDescent");
181: return intCellDescent;
182: }
183:
184: /// <summary>
185: /// Give the bounding box of a text using TrueType fonts
186: /// </summary>
187: /// <param name="font">The name of a TTF font.</param>
188: /// <param name="size">The size of the font.</param>
189: /// <param name="text">The text to be displayed using the font.</param>
190: /// <returns>An integer array containing the height and width.</returns>
191: /// <remarks>Not exactly the same as the original function, but the best we can do.</remarks>
192: public int[] ImageTTFBBox(string font, int size, string text)
193: {
194: Bitmap objBmpImage = new Bitmap(1, 1);
195:
196: int intWidth = 0;
197: int intHeight = 0;
198:
199: System.Drawing.Font objFont = new System.Drawing.Font(font, size, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Pixel);
200:
201: // Create a graphics object to measure the text’s width and height.
202: Graphics objGraphics = Graphics.FromImage(objBmpImage);
203:
204: // Determine the bitmap size.
205: /* Need to multiply the width by 1.25.
206: * This is an ugly hack.
207: * The System.Drawing width will not match the GD Library drawing width.
208: */
209: intWidth = Convert.ToInt32(objGraphics.MeasureString(text, objFont).Width * 1.25);
210: intHeight = Convert.ToInt32(objGraphics.MeasureString(text, objFont).Height);
211: Trace.WriteLine(intWidth.ToString(), "intWidth");
212: Trace.WriteLine(intHeight.ToString(), "intHeight");
213: int[] box = { intWidth, intHeight };
214: return box;
215: }
216:
217: /// <summary>
218: /// Generates a MD5 hash for an unique file name
219: /// </summary>
220: /// <param name="font_name">The name of a TTF font.</param>
221: /// <param name="font_size">The size of the font.</param>
222: /// <param name="font_color">The color of the font.</param>
223: /// <param name="background_color">The background color of the text image.</param>
224: /// <param name="transparent_color">The transparent color of the text image.</param>
225: /// <param name="text">The text of the text image.</param>
226: /// <returns>A string to be used as an unique file name.</returns>
227: private string GenerateMD5Hash(string font_name, string font_size, string font_color, string background_color, string transparent_background, string text)
228: {
229: // Create an instance of the MD5CryptoServiceProvider class
230: MD5CryptoServiceProvider md5Hasher = new MD5CryptoServiceProvider();
231: // The array of bytes that will contain the encrypted value
232: byte[] hashedBytes;
233: UTF8Encoding encoder = new UTF8Encoding();
234: // Call ComputeHash, passing in the plain-text string as an array of bytes
235: // The return value is the encrypted value, converted to a string
236: hashedBytes = md5Hasher.ComputeHash(encoder.GetBytes((font_name + font_size + font_color + background_color + transparent_background + text)));
237: Trace.WriteLine(BitConverter.ToString(hashedBytes).Replace("-", "").ToLower(), "BitConverter");
238: return BitConverter.ToString(hashedBytes).Replace("-", "").ToLower();
239: }
240:
241: /// <summary>
242: /// Reads data from a stream until the end is reached. The
243: /// data is returned as a byte array. An IOException is
244: /// thrown if any of the underlying IO calls fail.
245: /// </summary>
246: /// <param name="stream">The stream to read data from</param>
247: public static byte[] ReadFully(Stream stream)
248: {
249: byte[] buffer = new byte[32768];
250: using (MemoryStream ms = new MemoryStream())
251: {
252: while (true)
253: {
254: int read = stream.Read(buffer, 0, buffer.Length);
255: if (read <= 0)
256: return ms.ToArray();
257: ms.Write(buffer, 0, read);
258: }
259: }
260: }
261:
262: public bool IsReusable {
263: get {
264: return false;
265: }
266: }
267:
268: }