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