I've been working on a financial reporting project lately and came upon the idea
of bringing our charts from ChartFX.NET to life. The idea being that we could
dynamically review each pixel's color in the chart in order to generate an HTML
image map on the fly. This would enable us to apply supporting documentation
in the form of links or JavaScript functions to perform some sort of action when
the user moves their mouse over the chart or clicks a section of it.
.NET offers GDI+ as its tool for reviewing and manipulating images. We can also
implement unsafe code to work with images. Since we try to avoid code marked
as unsafe here, I opted to just use the Bitmap object's .GetPixel and .SetPixel
methods instead. If you have the option to implement unsafe code, you'll want
to look into .LockBits and .UnLockBits. These two methods are known to be faster
for working with images.
In a nutshell, the code sample (formatted nicely for reading) below takes in an List of structs, an image tag element name, and the relative filename of the image. The
struct holds the red, green, and blue desired colors to find in the chart/image
and generate the proper area tag. My first attempt at this was to generate an
area tag for each and every pixel. Naturally, this created a huge amount of
HTML for the larger images (400x400) that I was testing with. The next logical
step was to try and reduce the number of tags written by determining areas on
a line by line basis in the 400 x 400 grid of pixels.
The code below iterates through the List of structs, reviews the desired color, searches line by line and pixel by pixel
looking for color matches. If it finds the desired color, it sets a marker and
increments that pixel marker until it runs across a different pixel color in
the image. When this occurs, it writes the area tag with that line's coordinates
and sets a new marker. I chose the line by line method of iterating through
the pixels in order to support very complex images typically generated from charting
software.
For my charts and stress testing, this proved to work pretty well. You'll find that
the slowest part of this process is the .GetPixel method itself. This is why
I make one pass through to collect the colors of the image and store it in a
multi-dimensional array sized to match the image height and width. Then, reference
the array by the pixel index each time we process a desired color.
ImageMaps.cs
using System;
using System.Data;
using System.Collections;
using System.Drawing;
using System.Web;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Text;
namespace EggHeadCafe.Controls
{
#region Structs
public struct UrlsForImageColor
{
public string Red;
public string Green;
public string Blue;
public string Url;
public string AltTag;
public string Target;
public string OnMouseOver;
}
#endregion
public class ImageMaps
{
public ImageMaps()
{
}
#region Get Map
public string GetMap(string ImageElementName,string ImageFileName,List<EggHeadCafe.Controls.UrlsForImageColor> LinksByColor)
{
string sRet="";
string Coords="";
string CurColor="";
string CompareColor="";
bool fFoundColorInPixel=false;
int hplus=0;
int Start=0;
int End=0;
int y=0;
int x=0;
int u=0;
Color pixelColor;
var sb = new StringBuilder();
EggHeadCafe.Controls.UrlsForImageColor oLink;
Bitmap oImage = null;
try
{
ImageFileName = System.Web.HttpContext.Current.Server.MapPath(ImageFileName);
oImage = new Bitmap(System.Drawing.Image.FromFile(ImageFileName));
y = oImage.Height;
x = oImage.Width;
string[,] ImagePixels = new string[y,x];
for(int h = 0; h < y; h++)
{
for(int w = 0; w < x; w++)
{
pixelColor = oImage.GetPixel(h,w);
ImagePixels[h,w] = pixelColor.R.ToString() + "." + pixelColor.G.ToString() + "." + pixelColor.B.ToString();
}
}
sb.Append("<img name=\"" + ImageElementName + "\" src=\"" + ImageFileName + "\" width=\"" + x.ToString());
sb.Append("\" height=\"" + y.ToString() + "\" border=\"0\" usemap=\"#m_" + ImageElementName + "2\">" + "\n");
sb.Append("<map name=\"m_" + ImageElementName + "2\">" + "\n");
for(u = 0; u < LinksByColor.Count; u++)
{
oLink = (EggHeadCafe.Controls.UrlsForImageColor)LinksByColor[u];
CompareColor = oLink.Red + "." + oLink.Green + "." + oLink.Blue;
for(int h = 0; h < y; h++)
{
for(int w = 0; w < x; w++)
{
if (w==0) { Start = w; }
CurColor = ImagePixels[h,w];
if (CurColor != CompareColor)
{
if (fFoundColorInPixel == true)
{
Coords = h.ToString() + ",";
Coords += Start.ToString() + ",";
Coords += hplus.ToString() + ",";
Coords += End.ToString();
sb.Append(WriteArea(Coords,oLink));
}
Start = w;
fFoundColorInPixel = false;
continue;
}
else
{
fFoundColorInPixel = true;
}
End = w;
hplus = h + 1;
} // End FOR width
if (fFoundColorInPixel == true)
{
Coords = h.ToString() + ",";
Coords += Start.ToString() + ",";
Coords += hplus.ToString() + ",";
Coords += End.ToString();
sb.Append(WriteArea(Coords,oLink));
}
}
}
sb.Append("</map>" + "\n");
}
catch
{
throw;
}
finally
{
sRet = sb.ToString();
oImage.Dispose();
}
return sRet;
}
#endregion
#region Write Area
private string WriteArea(string Coords,EggHeadCafe.Controls.UrlsForImageColor oLink)
{
string sRet="";
sRet = "<area shape=\"rect\" coords=\"" + Coords + "\"";
if (oLink.Url.Length > 0) { sRet += " href=\"" + oLink.Url + "\" "; }
if (oLink.AltTag.Length > 0) { sRet += " alt=\"" + oLink.AltTag + "\""; }
if (oLink.Target.Length > 0) { sRet += " target=\"" + oLink.Target + "\""; }
if (oLink.OnMouseOver.Length > 0) { sRet += " onmouseover=\"" + oLink.OnMouseOver + "\""; }
sRet += ">\n";
return sRet;
}
#endregion
}
}
ImageMaps.aspx
<%@ Import Namespace="System.Drawing" %>
<%@ Import Namespace="System.Drawing.Drawing2D" %>
<%@ Import Namespace="System.Drawing.Imaging" %>
<%@ Import Namespace="EggHeadCafe.Controls" %>
<script Language="C#" runat="server">
protected void Page_Load(object sender, EventArgs e)
{
var oMap = new EggHeadCafe.Controls.ImageMaps();
var oLinks = new List<EggHeadCafe.Controls.UrlsForImageColor>();
EggHeadCafe.Controls.UrlsForImageColor oLink;
try
{
oLink = new EggHeadCafe.Controls.UrlsForImageColor();
oLink.Red = "255";
oLink.Green = "0";
oLink.Blue = "0";
oLink.Url = "http://www.eggheadcafe.com";
oLink.AltTag = "my red region";
oLink.Target = "_blank";
oLink.OnMouseOver = "";
oLinks.Add(oLink);
oLink = new EggHeadCafe.Controls.UrlsForImageColor();
oLink.Red = "0";
oLink.Green = "0";
oLink.Blue = "0";
oLink.Url = "http://www.robbemorris.com";
oLink.AltTag = "my black region";
oLink.Target = "_blank";
oLink.OnMouseOver = "";
Links.Add(oLink);
Response.Write(oMap.GetMap("Image1","images/pie.gif",Links));
Links.Clear();
oLink = new EggHeadCafe.Controls.UrlsForImageColor();
oLink.Red = "0";
oLink.Green = "0";
oLink.Blue = "204";
oLink.Url = "http://www.eggheadcafe.com";
oLink.AltTag = "blue region";
oLink.Target = "_blank";
oLink.OnMouseOver = "alert('over blue region');";
Links.Add(oLink);
oLink = new EggHeadCafe.Controls.UrlsForImageColor();
oLink.Red = "255";
oLink.Green = "0";
oLink.Blue = "255";
oLink.Url = "http://www.asp.net";
oLink.AltTag = "pink region";
oLink.Target = "_blank";
oLink.OnMouseOver = "";
Links.Add(oLink);
oLink = new EggHeadCafe.Controls.UrlsForImageColor();
oLink.Red = "102";
oLink.Green = "0";
oLink.Blue = "153";
oLink.Url = "http://www.yahoo.com";
oLink.AltTag = "purple region";
oLink.Target = "_blank";
oLink.OnMouseOver = "";
Links.Add(oLink);
oLink = new EggHeadCafe.Controls.UrlsForImageColor();
oLink.Red = "204";
oLink.Green = "255";
oLink.Blue = "0";
oLink.Url = "http://www.google.com";
oLink.AltTag = "yellow";
oLink.Target = "_blank";
oLink.OnMouseOver = "";
Links.Add(oLink);
oLink = new EggHeadCafe.Controls.UrlsForImageColor();
oLink.Red = "102";
oLink.Green = "153";
oLink.Blue = "102";
oLink.Url = "http://www.rushlimbaugh.com";
oLink.AltTag = "green region";
oLink.Target = "_blank";
oLink.OnMouseOver = "";
Links.Add(oLink);
oLink = new EggHeadCafe.Controls.UrlsForImageColor();
oLink.Red = "204";
oLink.Green = "204";
oLink.Blue = "255";
oLink.Url = "http://www.foxnews.com";
oLink.AltTag = "light purple";
oLink.Target = "_blank";
oLink.OnMouseOver = "";
Links.Add(oLink);
Response.Write(oMap.GetMap("Image2","images/region.gif",Links));
}
catch (Exception err) { Response.Write(err.Message); }
}
</script>