CSS Sprite for ASP.NET
CSS sprites are becoming popular as a way to increase application performance by eliminating HTTP requests by the client to the web server. It also serves as a path for better cache management. I will need to go through a bit of background before we start (sorry... we'll get to the generation in a second!).
What is a CSS Sprite?
A CSS sprite is a fancy name for an image that is composed other images. In your CSS you have this one base image, but you pick the image inside of the image. It does what you expect, to just show that one single image. The following is an example of a CSS sprite:
In your CSS file, you would pick this base image as your background-image and then to pick a particular image, you could need to specify the height, width, and also the position of where the image starts... background-position.
Long story short, it cuts down on the number of images that need to be passed from your server to the client that is visiting the site. This adds up over time, especially is you have A LOT of images.
A lot of people go to a sprite generator online. Those are nice, but what happens when you need to change an image? You have to go back and re-upload all your images and recalculate your background-positions. That's not a good thing if you are following DRY (don't repeat yourself). Plus it's hard work NO ONE should have to do.
How I handle output caching
What I do is this: all my CSS files, JavaScript files, etc have a common ID which is basically what I call the "Distro ID" which is generated at the beginning of my ASP.NET applications so every time you do a publish, the number will change and your files will also change to their updated content. The heavy lifting is all done in the Global.asax file.
1: //C#
2: public class Global : System.Web.HttpApplication {
3: public static string DistributionNumber { get; set; }
4:
5: protected void Application_Start(object sender, EventArgs e){
6: SetDistro();
7: ..
8: }
9: private void SetDistro() {
10: DistributionNumber = DateTime.Now.Year.ToString() + DateTime.Now.Month.ToString() + DateTime.Now.Day.ToString() + DateTime.Now.Hour.ToString() + DateTime.Now.Minute.ToString() + DateTime.Now.Second.ToString();
11: }
12: }
1: 'VB
2: Inherits System.Web.HttpApplication
3:
4: Private m_distronum as String = String.Empty
5: Public Shared Property DistributionNumber() As String
6: Get
7: Return m_distronum
8: End Get
9: Set(ByVal value as String)
10: m_distronum = value
11: End Set
12: End Property
13:
14: Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
15: SetDistro()
16: ..
17: End Sub
18: Private Sub SetDistro()
19: DistributionNumber = DateTime.Now.Year.ToString() + DateTime.Now.Month.ToString() + DateTime.Now.Day.ToString() + DateTime.Now.Hour.ToString() + DateTime.Now.Minute.ToString() + DateTime.Now.Second.ToString()
20: End Sub
Now since I am mostly using ASP.NET MVC, I have a controller for the Distribution. So my URLs look like this: [BASE_URL]/20080303010101/CSS. I ago ahead and create separate Routes for this, but the default would also work. The ID that is passed to the CSS, JavaScript, Sprite is the distro number. To do the output caching, you need a separate ID so that the browser can differentiate one distribution's CSS, JS, etc from another. This way, we can client side cache with no worries.
If you were using Web Forms, I would suggest using separate Generic handlers (one for CSS, JS, and the Sprite). They would accomplish the same thing. Just remember that you need to take in an ID as a Query String for the Client-side cache to be safe for your users.
If the ID is not passed to the controller OR the generic handler (whichever you are using), don't client-side cache. But this is something you will want to offer as it, too, saves you in the performance department.
Back to the Sprite generator... get me some images!
You have 2 options when it comes to getting images. You have the option of enumerating through a folder(s) or putting the URLs in by hand. I suggest enumeration if you have the same file type. If you put in the file names by hand, that can get tricky... which I'm sure you can tell why. In my Sprite generator, I process the CSS and replace the images with the sprite reference... so we don't need the URL of the original images anyway's. All that we need to reference the images is the name (lowercase).
As I showed you in my last post about CSS Minification (which we will refer back to), I have a file enumerator method:
1: //C#
2: public static IList<System.IO.FileInfo> GetFiles(string serverPath, string extention)
3: {
4: if (!serverPath.StartsWith("~/"))
5: {
6: if (serverPath.StartsWith("/"))
7: serverPath = "~" + serverPath;
8: else
9: serverPath = "~/" + serverPath;
10: }
11: string path = HttpContext.Current.Server.MapPath(serverPath);
12:
13: if (!path.EndsWith("/"))
14: path = path + "/";
15:
16: if (!Directory.Exists(path))
17: throw new System.IO.DirectoryNotFoundException();
18:
19: IList<FileInfo> files = new List<FileInfo>();
20:
21: string[] fileNames = Directory.GetFiles(path, "*." + extention, System.IO.SearchOption.AllDirectories);
22:
23: foreach (string name in fileNames)
24: files.Add(new FileInfo(name));
25:
26: return files;
27: }
1: 'VB
2: Public Shared Function GetFiles(ByVal serverPath As String, ByVal extention As String) As IList(Of System.IO.FileInfo)
3: If Not serverPath.StartsWith("~/") Then
4: If serverPath.StartsWith("/") Then
5: serverPath = "~" + serverPath
6: Else
7: serverPath = "~/" + serverPath
8: End If
9: End If
10: Dim path As String = HttpContext.Current.Server.MapPath(serverPath)
11:
12: If Not path.EndsWith("/") Then
13: path = path + "/"
14: End If
15:
16: If Not Directory.Exists(path) Then
17: Throw New System.IO.DirectoryNotFoundException()
18: End If
19:
20: Dim files As IList(Of FileInfo) = New List(Of FileInfo)()
21:
22: Dim fileNames As String() = Directory.GetFiles(path, "*." + extention, System.IO.SearchOption.AllDirectories)
23:
24: For Each name As String In fileNames
25: files.Add(New FileInfo(name))
26: Next
27:
28: Return files
29: End Function
If you were going the hand-coded image URLs, you would just need to have a method that return a List of FileInfo, just like the method from above.
Get the SPRITE already!
We need to specify a few data models so that they can contain the individual images and the sprites. They aren't anything special... just basically containers for the bitmap object (SpriteImage) and a Collection of the images for the Sprite generation (SpriteImageCollection). The collection is actually the generator of the sprite. The GetSprite() method is the method that will do the image concatenating for you.
1: //C#
2: public class SiteImage
3: {
4: public SiteImage(FileInfo fileinfo)
5: {
6: Stream stream = new FileStream(fileinfo.FullName, FileMode.Open, FileAccess.Read);
7: Img = (Bitmap)Bitmap.FromStream(stream);
8: File = fileinfo;
9: }
10:
11: public int Width {
12: get { return Img.Width; }
13: }
14: public int Height {
15: get { return Img.Height; }
16: }
17: public int YValue { get; set; }
18: public Bitmap Img { get; set; }
19: public FileInfo File { get; set; }
20: }
21:
22: public class SiteImageCollection : List<SiteImage>
23: {
24: public int MaxWidth
25: {
26: get
27: {
28: int largest = 0;
29:
30: foreach (SiteImage s in this)
31: if (s.Width > largest)
32: largest = s.Width;
33:
34: return largest;
35: }
36: }
37:
38: public int TotalHeight
39: {
40: get
41: {
42: int ttl = 0;
43: foreach (SiteImage s in this)
44: ttl += s.Height;
45: return ttl;
46: }
47: }
48:
49: public Bitmap GetSprite()
50: {
51: Bitmap spriteImg = new Bitmap(MaxWidth, TotalHeight);
52:
53: Graphics sprite = Graphics.FromImage(spriteImg);
54:
55: int curY = 0;
56: foreach (SiteImage si in this)
57: {
58: sprite.DrawImage(si.Img, 0, curY);
59: si.YValue = curY;
60: curY += si.Height;
61: }
62:
63: return spriteImg;
64: }
65: }
1: 'VB
2: Public Class SiteImage
3: Public Sub New(ByVal fileinfo As FileInfo)
4: Dim stream As Stream = New FileStream(fileinfo.FullName, FileMode.Open, FileAccess.Read)
5: Img = DirectCast(Bitmap.FromStream(stream), Bitmap)
6: File = fileinfo
7: End Sub
8:
9: Public ReadOnly Property Width() As Integer
10: Get
11: Return Img.Width
12: End Get
13: End Property
14:
15: Public ReadOnly Property Height() As Integer
16: Get
17: Return Img.Height
18: End Get
19: End Property
20:
21: Private m_yval As Integer
22: Public Property YValue() As Integer
23: Get
24: Return m_yval
25: End Get
26: Set(ByVal value As Integer)
27: m_yval = value
28: End Set
29: End Property
30:
31: Private m_Img As Bitmap
32: Public Property Img() As Bitmap
33: Get
34: Return m_Img
35: End Get
36: Set(ByVal value As Bitmap)
37: m_Img = value
38: End Set
39: End Property
40:
41: Private m_file As FileInfo
42: Public Property File() As FileInfo
43: Get
44: Return m_file
45: End Get
46: Set(ByVal value As FileInfo)
47: m_file = value
48: End Set
49: End Property
50: End Class
51:
52: Public Class SiteImageCollection
53: Inherits List(Of SiteImage)
54: Public ReadOnly Property MaxWidth() As Integer
55: Get
56: Dim largest As Integer = 0
57:
58: For Each s As SiteImage In Me
59: If s.Width > largest Then
60: largest = s.Width
61: End If
62: Next
63:
64: Return largest
65: End Get
66: End Property
67:
68: Public ReadOnly Property TotalHeight() As Integer
69: Get
70: Dim ttl As Integer = 0
71: For Each s As SiteImage In Me
72: ttl += s.Height
73: Next
74: Return ttl
75: End Get
76: End Property
77:
78: Public Function GetSprite() As Bitmap
79: Dim spriteImg As New Bitmap(MaxWidth, TotalHeight)
80:
81: Dim sprite As Graphics = Graphics.FromImage(spriteImg)
82:
83: Dim curY As Integer = 0
84: For Each si As SiteImage In Me
85: sprite.DrawImage(si.Img, 0, curY)
86: si.YValue = curY
87: curY += si.Height
88: Next
89:
90: Return spriteImg
91: End Function
92: End Class
There you have it. A nice sprite generated from existing images. The cool thing is that if you are working with PNGs, your transparencies are preserved, which is what I really needed. The only real down side to this is that all your images are lined up vertically. I'm not quite sure that this matters, but most other generators have the ability to somehow line the images up vertically and horizontally.
But you're not exactly done...
You still have the CSS to deal with...
CSS sprites don't have CSS in the name for nothing. They are best used in a CSS files. Lucky for me, I already have a process for my CSS files to go through (which includes minification, cache, etc). All I need to do is augment it to replace the image reference to the actual URL of the sprite and to calculate the background-position.
Logic Class
I really like having Logic classes (which you have seen in my past blog posts) because they are a pivotal point between the server-side cache mechanisms that I put into place. Here's what my Logic controller looks like:
1: //C#
2: public class CssSprite
3: {
4: public static SiteImageCollection GetImagesForSprite()
5: {
6: SiteImageCollection col = Helpers.UrlMapping.UrlHandler.Images.AllImages();
7: //we need to grab the sprite so that the Y-values are usable
8: Bitmap sprite = col.GetSprite();
9: return col;
10: }
11:
12: public static Bitmap GetCachedSprite()
13: {
14: ICache<SiteImageCollection> cache = new Helpers.Cache.CssSpritCache();
15:
16: return cache.Get().GetSprite();
17: }
18:
19: public static string CleanCSS(string currentCSS)
20: {
21: string newCSS = currentCSS;
22: foreach(SiteImage si in CssSprite.GetImagesForSprite())
23: {
24:
25: string orig = "background-image: url(" + si.File.Name.ToLower() + ");";
26:
27: newCSS = newCSS.Replace(orig + "/**/", ReplacementCss(si, false));
28: newCSS = newCSS.Replace(orig, ReplacementCss(si, true));
29: }
30: return newCSS;
31: }
32:
33: private static string ReplacementCss(SiteImage si, bool widthheight)
34: {
35: string rep = "background-image: url(" + Helpers.UrlMapping.UrlHandler.Images.Sprite().ToAbsoluteURL() + ");";
36: rep += "background-position: 0px -" + si.YValue + "px;";
37: if (widthheight)
38: {
39: rep += "width: " + si.Width + ";";
40: rep += "height: " + si.Height + ";";
41: }
42: return rep;
43: }
44: }
1: 'VB
2: Public Class CssSprite
3: Public Shared Function GetImagesForSprite() As SiteImageCollection
4: Dim col As SiteImageCollection = Helpers.UrlMapping.UrlHandler.Images.AllImages()
5: 'we need to grab the sprite so that the Y-values are usable
6: Dim sprite As Bitmap = col.GetSprite()
7: Return col
8: End Function
9:
10: Public Shared Function GetCachedSprite() As Bitmap
11: Dim cache As ICache(Of SiteImageCollection) = New Helpers.Cache.CssSpritCache()
12:
13: Return cache.[Get]().GetSprite()
14: End Function
15:
16: Public Shared Function CleanCSS(ByVal currentCSS As String) As String
17: Dim newCSS As String = currentCSS
18: For Each si As SiteImage In CssSprite.GetImagesForSprite()
19:
20: Dim orig As String = "background-image: url(" + si.File.Name.ToLower() + ");"
21:
22: newCSS = newCSS.Replace(orig + "/**/", ReplacementCss(si, False))
23: newCSS = newCSS.Replace(orig, ReplacementCss(si, True))
24: Next
25: Return newCSS
26: End Function
27:
28: Private Shared Function ReplacementCss(ByVal si As SiteImage, ByVal widthheight As Boolean) As String
29: Dim rep As String = "background-image: url(" + Helpers.UrlMapping.UrlHandler.Images.Sprite().ToAbsoluteURL() + ");"
30: rep &= "background-position: 0px -" + si.YValue + "px;"
31: If widthheight Then
32: rep &= "width: " + si.Width + ";"
33: rep &= "height: " + si.Height + ";"
34: End If
35: Return rep
36: End Function
37: End Class
So what this does is replace the values where the background-image is equal to an image in the sprite's name. It then appends the style definition to add the background-position (which we found during the Sprite generation) and the height and width and changes the background-image URL. If you add a /**/ right before the background-image definition, the height and width are not added in.
The only thing that I didn't really show you was the AllImages() method (which returns a SiteImageCollection on the images I need sprited) and the Sprite() method in my UrlHandler. That's not that important... I'm sure you can figure out what those do :)
Cache mechanism
When we last talked about my CacheBase class, it was sort of all over the place. I have revamped it to use delegates so that we don't get any weird methods that don't make sense. It's still a little messy since I needed to write 2 classes, one that took 1 generic and another that took 2. Here it is:
1: //C#
2: public abstract class CacheBase<T> : ICache<T>
3: {
4: public abstract Func<T> Method { get; }
5: public abstract string CacheKey { get; }
6: public abstract CacheItemPriority Priority { get; }
7: public abstract TimeSpan CacheDuration { get; }
8:
9: public T Get() {
10: T CurValue = ((T)HttpContext.Current.Cache[CacheKey]);
11:
12: if (CurValue == null)
13: CurValue = Invoke();
14:
15: return CurValue;
16: }
17:
18: /// <summary>
19: /// Removes the Cache Object from the
20: /// current cache.
21: /// </summary>
22: public void Delete(){
23: HttpContext.Current.Cache.Remove(CacheKey);
24: }
25:
26: private T Invoke(){
27: return Method.Invoke();
28: }
29:
30: /// <summary>
31: /// Adds the value into the Cache
32: /// </summary>
33: /// <param name="Value">Value of T</param>
34: internal T Insert(T Value)
35: {
36: HttpContext.Current.Cache.Add(CacheKey, Value, null, DateTime.Now.Add(CacheDuration), TimeSpan.Zero, Priority, null);
37: return Value;
38: }
39: }
40:
41: public abstract class CacheBase<T, P1> : ICache<T>
42: {
43: public abstract Func<P1, T> Method { get; }
44: public abstract P1 ObjectDescripter { get; }
45: public abstract string CacheKey { get; }
46: public abstract CacheItemPriority Priority { get; }
47: public abstract TimeSpan CacheDuration { get; }
48:
49: public T Get(){
50: T CurValue = ((T)HttpContext.Current.Cache[CacheKey]);
51:
52: if (CurValue == null)
53: CurValue = Invoke();
54:
55: return CurValue;
56: }
57:
58: /// <summary>
59: /// Removes the Cache Object from the
60: /// current cache.
61: /// </summary>
62: public void Delete(){
63: HttpContext.Current.Cache.Remove(CacheKey);
64: }
65:
66: private T Invoke(){
67: return Method.Invoke(ObjectDescripter);
68: }
69:
70: /// <summary>
71: /// Adds the value into the Cache
72: /// </summary>
73: /// <param name="Value">Value of T</param>
74: internal T Insert(T Value){
75: HttpContext.Current.Cache.Add(CacheKey, Value, null, DateTime.Now.Add(CacheDuration), TimeSpan.Zero, Priority, null);
76: return Value;
77: }
78:
79: }
1: 'VB
2: Public MustInherit Class CacheBase(Of T)
3: Implements ICache(Of T)
4: Public MustOverride ReadOnly Property Method() As Func(Of T)
5: Public MustOverride ReadOnly Property CacheKey() As String
6: Public MustOverride ReadOnly Property Priority() As CacheItemPriority
7: Public MustOverride ReadOnly Property CacheDuration() As TimeSpan
8:
9: Public Function [Get]() As T
10: Dim CurValue As T = (DirectCast(HttpContext.Current.Cache(CacheKey), T))
11:
12: If CurValue Is Nothing Then
13: CurValue = Invoke()
14: End If
15:
16: Return CurValue
17: End Function
18:
19: ''' <summary>
20: ''' Removes the Cache Object from the
21: ''' current cache.
22: ''' </summary>
23: Public Sub Delete()
24: HttpContext.Current.Cache.Remove(CacheKey)
25: End Sub
26:
27: Private Function Invoke() As T
28: Return Method.Invoke()
29: End Function
30:
31: ''' <summary>
32: ''' Adds the value into the Cache
33: ''' </summary>
34: ''' <param name="Value">Value of T</param>
35: Friend Function Insert(ByVal Value As T) As T
36: HttpContext.Current.Cache.Add(CacheKey, Value, Nothing, DateTime.Now.Add(CacheDuration), TimeSpan.Zero, Priority, _
37: Nothing)
38: Return Value
39: End Function
40:
41: End Class
42:
43: Public MustInherit Class CacheBase(Of T, P1)
44: Implements ICache(Of T)
45: Public MustOverride ReadOnly Property Method() As Func(Of P1, T)
46: Public MustOverride ReadOnly Property ObjectDescripter() As P1
47: Public MustOverride ReadOnly Property CacheKey() As String
48: Public MustOverride ReadOnly Property Priority() As CacheItemPriority
49: Public MustOverride ReadOnly Property CacheDuration() As TimeSpan
50:
51: Public Function [Get]() As T
52: Dim CurValue As T = (DirectCast(HttpContext.Current.Cache(CacheKey), T))
53:
54: If CurValue Is Nothing Then
55: CurValue = Invoke()
56: End If
57:
58: Return CurValue
59: End Function
60:
61: ''' <summary>
62: ''' Removes the Cache Object from the
63: ''' current cache.
64: ''' </summary>
65: Public Sub Delete()
66: HttpContext.Current.Cache.Remove(CacheKey)
67: End Sub
68:
69: Private Function Invoke() As T
70: Return Method.Invoke(ObjectDescripter)
71: End Function
72:
73: ''' <summary>
74: ''' Adds the value into the Cache
75: ''' </summary>
76: ''' <param name="Value">Value of T</param>
77: Friend Function Insert(ByVal Value As T) As T
78: HttpContext.Current.Cache.Add(CacheKey, Value, Nothing, DateTime.Now.Add(CacheDuration), TimeSpan.Zero, Priority, _
79: Nothing)
80: Return Value
81: End Function
82:
83: End Class
So the Sprite cache looks like this:
1: //C#
2: public class CssSpritCache : CacheBase<SiteImageCollection>
3: {
4: public CssSpritCache()
5: {
6: }
7:
8: public override Func<SiteImageCollection> Method
9: {
10: get { return new Func<SiteImageCollection>(Logic.CssSprite.GetImagesForSprite); }
11: }
12:
13: public override string CacheKey
14: {
15: get { return "CssSprite_" + Global.DistributionNumber; }
16: }
17:
18: public override System.Web.Caching.CacheItemPriority Priority
19: {
20: get { return System.Web.Caching.CacheItemPriority.Default; }
21: }
22:
23: public override TimeSpan CacheDuration
24: {
25: get { return new TimeSpan(1, 0, 0, 0); }
26: }
27: }
1: 'VB
2: Public Class CssSpritCache
3: Inherits CacheBase(Of SiteImageCollection)
4: Public Sub New()
5: End Sub
6:
7: Public Overloads Overrides ReadOnly Property Method() As Func(Of SiteImageCollection)
8: Get
9: Return New Func(Of SiteImageCollection)(Logic.CssSprite.GetImagesForSprite)
10: End Get
11: End Property
12:
13: Public Overloads Overrides ReadOnly Property CacheKey() As String
14: Get
15: Return "CssSprite_" + [Global].DistributionNumber
16: End Get
17: End Property
18:
19: Public Overloads Overrides ReadOnly Property Priority() As System.Web.Caching.CacheItemPriority
20: Get
21: Return System.Web.Caching.CacheItemPriority.[Default]
22: End Get
23: End Property
24:
25: Public Overloads Overrides ReadOnly Property CacheDuration() As TimeSpan
26: Get
27: Return New TimeSpan(1, 0, 0, 0)
28: End Get
29: End Property
30: End Class
Back in the CSS logic handler, you need to change the CombineCSS method to also do the replacement for the CSS Sprite.
1: //C#
2: public static string CombineCSS()
3: {
4: string allCSS = string.Empty;
5:
6: foreach (FileInfo fi in Logic.Files.GetFiles("~/Content/CSS/", "css"))
7: {
8: using (StreamReader sr = new StreamReader(fi.FullName))
9: allCSS += sr.ReadToEnd();
10: }
11:
12: allCSS = CssSprite.CleanCSS(allCSS);
13:
14: allCSS = Compress(allCSS);
15:
16: return allCSS;
17: }
1: 'VB
2: Public Shared Function CombineCSS() As String
3: Dim allCSS As String = String.Empty
4:
5: For Each fi As FileInfo In Logic.Files.GetFiles("~/Content/CSS/", "css")
6: Using sr As New StreamReader(fi.FullName)
7: allCSS += sr.ReadToEnd()
8: End Using
9: Next
10:
11: allCSS = CssSprite.CleanCSS(allCSS)
12:
13: allCSS = Compress(allCSS)
14:
15: Return allCSS
16: End Function
Are you done?
You're sort of done. I've only given you the loose bits of the solution. You will have to display the CSS Sprite onto and image. I didn't show you my code for the actual CSS Sprite MVC Controller Action/Generic Handler. This is needed for you to use the CSS Sprite, after all.
1: //C#
2:
3: //--------MVC--------
4: [ControllerAction]
5: public void Sprite(string id)
6: {
7: Response.ContentType = "image/png";
8:
9: Bitmap bmp = Logic.CssSprite.GetCachedSprite();
10:
11: MemoryStream stream = new MemoryStream();
12: bmp.Save(stream, System.Drawing.Imaging.ImageFormat.Png);
13:
14: Response.BinaryWrite(stream.ToArray());
15:
16: if(id != null)
17: Response.Cache.SetExpires(DateTime.Now.AddYears(3));
18: }
19:
20: //-------WebForms-----
21: public void ProcessRequest(HttpContext context)
22: {
23: context.Response.ContentType = "image/png";
24:
25: Bitmap bmp = Logic.CssSprite.GetCachedSprite();
26:
27: MemoryStream stream = new MemoryStream();
28: bmp.Save(stream, System.Drawing.Imaging.ImageFormat.Png);
29:
30: context.Response.BinaryWrite(stream.ToArray());
31:
32: if (context.Request.QueryString["id"] != null)
33: context.Response.Cache.SetExpires(DateTime.Now.AddYears(3));
34: }
1: 'VB
2: '--------MVC--------
3: <ControllerAction()> _
4: Public Sub Sprite(ByVal id As String)
5: Response.ContentType = "image/png"
6:
7: Dim bmp As Bitmap = Logic.CssSprite.GetCachedSprite()
8:
9: Dim stream As New MemoryStream()
10: bmp.Save(stream, System.Drawing.Imaging.ImageFormat.Png)
11:
12: Response.BinaryWrite(stream.ToArray())
13:
14: If Not id Is Nothing Then
15: Response.Cache.SetExpires(DateTime.Now.AddYears(3))
16: End If
17: End Sub
18:
19: '-------WebForms-----
20: Public Sub ProcessRequest(ByVal context As HttpContext)
21: context.Response.ContentType = "image/png"
22:
23: Dim bmp As Bitmap = Logic.CssSprite.GetCachedSprite()
24:
25: Dim stream As New MemoryStream()
26: bmp.Save(stream, System.Drawing.Imaging.ImageFormat.Png)
27:
28: context.Response.BinaryWrite(stream.ToArray())
29:
30: If Not context.Request.QueryString("id") Is Nothing Then
31: context.Response.Cache.SetExpires(DateTime.Now.AddYears(3))
32: End If
33: End Sub
OK, NOW you're done
There you have it. A nice, clean CSS Sprite generated for you. This should save you some time in the long run and will certainly help your users avoid waiting too long for your website to load. If, say, you were to put 10 images into the sprite, for instance, you would save your users 9 requests from the server. That is HUGE for performance. And if your user has a primed cache, it doesn't need to have any request to the server until your distro number changes.
So in the end, we all win with CSS sprites. You get them generated for you at runtime, your users don't have to wait, and your website will be leaner and meaner!