Thursday, October 2, 2008

Manejo dinámico de imágenes

En todos los proyectos web hay que lidiar con imágenes, iconos, thumbnails, etc. y siempre es tedioso y poco productivo tener que armar versiones de la misma imagen para mostrarlas en diferentes tamaños, agregarles marcas de agua o transformarlas a escala de grises.

En este post voy mostrare una implementación de un System.Web.IHttpHandler que realiza modificaciones “on the fly” de imágenes.

Este Handler funciona recibiendo parametros en la url y estos parámetros le indican el archivo a procesar y que transformaciones deben ejecutarse. Las transformaciones implementadas son las siguientes:

  • Convertir a escala de grises.
  • Resizado.
  • Reemplazo de un color por otro.
  • Agregado de una marca de agua.

Antes de ver el codigo vamos a ver la referencia de estos parametros de url:

  • url: (obligatorio) System.Uri con la url del archivo, este url puede ser relativa o absoluta. Si se especifica absoluta la obtención del archivo se realizara via un request http.
  • outputformat: (opcional) mediante este parámetro se puede especificar cual va a ser el formato de salida de la imagen. Las opciones posibles son jpg, png y gif. Si se omite este parámetro la opción por defecto es png.
  • bw: (opcional) convierte la imagen a escala de grises. Las opciones válidas son true y false. Por defecto es false.
  • replacecolor: (opcional) reemplaza un color por otro. El formato del valor de este parámetro es colororigen_colordestino, los colores expresados en rgb. Ej: replacecolor=30,30,30_70,70,70.
  • resize: (opcional) genera una imagen al tamaño especificado. El formato del valor de este parámetro es ancho_alto. Ej: resize=200_100 para 200px de ancho por 100px de alto.
  • watermark: (opcional) permite agregar una marca de agua a la imagen especificada en el parámetro url. Los valores posibles de este parámetro son true y false, y cuando es true necesita de los siguientes parámetros:
      • watermarkurl: (obligatorio) url relativa o absoluta del archivo que actuará como marca de agua.
      • watermarkopacity: (opcional) define la opacidad de la marca de agua. Los valores posibles de este parámetro es cualquier entero entre 0 y 100. El valor por defecto es 50.
      • watermarkposition: (opcional) especifica la posición de la marca de agua en relacion a la imagen principal. Los valores posibles son TopLeft, TopRight, BottomRight, BottomLeft, Center. El valor por defecto es Center.
      • watermarksize: (opcional) especifica el tamaño de la marca de agua. El formato es el mismo que en el parámetro resize y si no se especifica se utiliza el tamaño original de la marca de agua.

Ahora sí, veamos un poco de codigo.

Como mencioné anteriormente, esta implementación esta hecha sobre un IHttpHandler. Aqui esta la declaración:

public class Handler : IHttpHandler

La interfaz IHttpHandler define la propiedad IsReusable y el método ProcessRequest que es el que realiza el proceso de los requests.

El metodo ProcessRequest empieza por validar el parámetro url que es obligatorio, sino lo encuentra o el no se puede obtener una imagen de esa url devuelve un error HTTP 400 Bad Request.

// declaro variables
Uri url = null;
// obtener parametro url
if (string.IsNullOrEmpty(context.Request.QueryString["url"]))
{
// request invalido.
context.Response.StatusCode = 400; //400 = bad request
context.Response.StatusDescription = "Request invalido. Falta parametro 'url'";
}
else
{
// parsear url
url = new Uri(context.Request.QueryString["url"], UriKind.RelativeOrAbsolute);
}

Para no procesar una y otra vez la misma imagen con los mismos parámetros, utilicé el sistema de cache de ASP.NET para almacenar el output del proceso de transformación, con lo cual al iniciar cada request hay que consultar si ya no se proceso un request igual:

string cacheKey = context.Request.QueryString.ToString().ToLowerInvariant();

byte[] cacheValue = (byte[])context.Cache.Get(cacheKey);
if (cacheValue != null)
{
// la image ya ha sido procesada anteriormente, escribir al buffer.
WriteToBuffer(context, cacheValue, url);

// sale
return;
}

Si no se ha procesado un request de iguales características, se prosigue con el proceso. El codigo de las transformaciones es bastante claro y directo, pero quiero resaltar el uso del método DrawImage de la clase System.Drawing.Graphics en vez del método System.Drawing.Image.GetThumbnailImage porque este último produce imagenes de poca calidad si el tamaño supera los 120 píxeles.


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