Saturday, September 20, 2008

CSS Sprites

Durante el desarrollo de www.aula365.com, a principio de 2007, en mi grupo de trabajo nos topamos con el problema de optimizar la cantidad de descargas que tenia que realizar el navegador para presentar el sitio.
Por la importante carga grafica del sitio entre iconos, bordes sombras, estilos y flashes estas sumaban mas de 200 y los navegadores en general solo permiten hacer 4 requests simultaneos a mismo dominio.

Buscando formas de abordar el problema, encontramos publicado en el sitio de desarrollo de yahoo una técnica llamada CSS Sprites que consiste en armar un lienzo con varias imagenes y luego cortarlas via clases CSS de manera de en vez de hacer 10 requests de 10kb, hacer uno solo de 100kb con todos los iconos del sitio.
Veamos un ejemplo:

La imagen sería la siguiente:


Y algunas clases de la hoja de estilos serian as:

.cssIcons_application {
background-image: url(sprite1.png);
width: 16px;
height: 16px;
overflow: hidden;
}
.cssIcons_application_add {
background-image: url(sprite1.png);
background-position: -16px top;
width: 16px;
height: 16px;
overflow: hidden;
}
.cssIcons_application_cascade {
background-image: url(sprite1.png);
background-position: -32px top;
width: 16px;
height: 16px;
overflow: hidden;
}

Y si quisiera mostrar algun icono lo haria asignando la clase CSS a un elemento DIV. Ej:

<div class="cssIcons_application"></div>



Si bien conceptualmente es bastante sencillo, la implementacion y el armado de los lienzos de imagenes y su hoja de estilos puede ser un trabajo bastante tedioso y muy propenso a errores.



Por este motivo desarrollamos un control ASP.NET que nos resuelva esta problematica.

Basicamente lo que hace el control es crear una tira de imagenes y su hoja de estilos a partir de un directorio especificado y que exponga esa informacion a traves de una una url administrada por un IHttpHandler.



Comencemos con la creacion de la imagen y la hoja de estilos.

Este trabajo lo realiza el metodo privado CreateSprite del control, que obtiene la lista de imagenes contenidas en el directorio especificado en la propiedad ImagesPath y arma las colecciones de archivos e imagenes.






FileInfo[] arrFiles = new DirectoryInfo(HttpContext.Current.Server.MapPath(_ImagesPath)).GetFiles();
List<FileInfo> listOfFiles = (from f in arrFiles
where string.Compare(f.Extension, ".png", StringComparison.OrdinalIgnoreCase) == 0
| string.Compare(f.Extension, ".gif", StringComparison.OrdinalIgnoreCase) == 0
| string.Compare(f.Extension, ".jpg", StringComparison.OrdinalIgnoreCase) == 0
select f).ToList();

// calcular todas las imagenes
Dictionary<FileInfo, System.Drawing.Image> listOfImages = new Dictionary<FileInfo, System.Drawing.Image>(listOfFiles.Count);
foreach (FileInfo imgFile in listOfFiles)
{
System.Drawing.Image img = System.Drawing.Image.FromFile(imgFile.FullName);
listOfImages.Add(imgFile, img);
}





Luego mediante una expresion Lambda calculamos el ancho y alto total del lienzo.






int totalWidth = listOfImages.Sum(o => o.Value.Width);
int totalHeight = listOfImages.Max(o => o.Value.Height);


Con esos datos calculados y utilizando la clase Bitmap y Graphics se genera la imagen. Por otro lado y en el mismo foreach se genera sobre un StringBuilder la hoja de estilos.






Bitmap canvas = new Bitmap(totalWidth, totalHeight);
Graphics gr = Graphics.FromImage(canvas);
int x = 0;
StringBuilder sbStyles = new StringBuilder();
string sImgUrl = this.GetSpriteImageUrl();
foreach (KeyValuePair<FileInfo, System.Drawing.Image> kvp in listOfImages)
{
// genero la clase css
string className = Path.GetFileNameWithoutExtension(kvp.Key.Name);
className = className.Replace(".", "_").ToLower();
className = string.Format("{0}_{1}", base.UniqueID, className);
sbStyles.Append("." + className + "\r\n{");
sbStyles.Append(string.Format("\tbackground-image: url({0});\r\n", sImgUrl));
if (x != 0)
{
sbStyles.Append(string.Format("\tbackground-position: -{0}px top;\r\n", x));
}
sbStyles.Append(string.Format("\twidth: {0}px;\r\n", kvp.Value.Width));
sbStyles.Append(string.Format("\theight: {0}px;\r\n", kvp.Value.Height));
sbStyles.Append("\toverflow: hidden;\r\n");
sbStyles.Append("}\r\n");

// genero la imagen
System.Drawing.Image img = kvp.Value;
gr.DrawImage(img, x, 0, img.Width, img.Height);

// sumo el offset
x += img.Width;
}




Una vez que se han creado la imagen y la hoja de estilos se persiste esa informacion en el cache de ASP.NET de manera que el handler que va a estar recibiendo los requests sobre otro thread pueda obtener la informacion.






MemoryStream ms = new MemoryStream();
canvas.Save(ms, ImageFormat.Png);
ms.Seek(0, SeekOrigin.Begin);
byte[] arr = new byte[ms.Length];
ms.Read(arr, 0, arr.Length);
ms.Close();

// persisto en el cache la imagen y la hoja de estilos
string sCacheKeyImage = string.Format("{0}image", this.UniqueID);
string sCacheKeyStyles = string.Format("{0}style", this.UniqueID);
base.Page.Cache.Add(sCacheKeyImage, arr, null, System.Web.Caching.Cache.NoAbsoluteExpiration, System.Web.Caching.Cache.NoSlidingExpiration, System.Web.Caching.CacheItemPriority.Normal, null);
base.Page.Cache.Add(sCacheKeyStyles, sbStyles.ToString(), null, System.Web.Caching.Cache.NoAbsoluteExpiration, System.Web.Caching.Cache.NoSlidingExpiration, System.Web.Caching.CacheItemPriority.Normal, null);



Toda esta generacion ocurre por primera vez y durante el event Init del control donde tambien se registra la hoja de estilos en el HEAD de la pagina.

En resumen, mediante esta tecnica es posible bajar la cantidad de requests que se necesitan para mostrar un set de iconos en una pagina.

Codigo fuente