Redimensionar imágenes, convertirlas a byte array y viceversa (con transparencia)

El título del post es algo largo, pero resume un problema que me volvía de cabeza desde hace un tiempo, y que no era capaz de resolver… hasta hoy.

Cuando trabajamos con imágenes en una aplicación suele ser muy común almacenarlas en una base de datos. En el caso que me ocupa, al ser imágenes con una resolución bastante alta, un requisito es que éstas deben almacenarse a distintas resoluciones. Sin embargo, antes de continuar con el tema permitidme un paréntesis:

<PARENTESIS MODE = “on”>

Sé que existen bastantes detractores de ésta práctica, que suelen preferir guardar las imágenes en disco, pero esto a mi juicio conlleva una serie de inconvenientes:

  1. Pérdida de atomicidad: Mezclamos un sistema transaccional con un sistema de ficheros no transaccional (y no, de momento no recomiendo usar transacciones en el sistema de ficheros, al menos si no queréis quedaros calvos en el proceso). De modo que como no disponemos de un mecanismo transaccional, debemos implementar mecanismos de sincronización ‘a manija’ entre la base de datos y el sistema de ficheros, con todo lo que conlleva.
  2. Problemas al hacer copias de seguridad: Ya que mediante el SQL Server agent podemos planificar copias periódicas de la base de datos, pero no de los ficheros asociados. Así pues, hay que copiar los ficheros manualmente o lanzando un proceso desde nuestra aplicación.
  3. También suele argumentarse que si guardamos las imágenes en la base de datos, el tamaño de la base de datos puede incrementarse mucho y degradarse el rendimiento (recordar que SQL Server Express ‘sólo’ admite bases de datos de hasta 10GB). Esto no es cierto, ya que desde la versión 2008 existe la posibilidad de utilizar FILESTREAM, que permite almacenar los datos de un campo en el sistema de ficheros, obteniendo así lo mejor de ambos mundos.

<PARENTESIS MODE= “off”>

Vale, sigamos con el tema.

Como os decía, en el proyecto que me ocupa actualmente un requisito muy importante es almacenar distintas resoluciones de una imagen en la base de datos mediante FILESTREAM. Para ello, hay que redimensionar cada una de las imágenes y convertirlas en un array de bytes, para luego almacenarlas en un campo de tipo BLOB, concretamente varbinary(MAX). Posteriormente cuando queremos recuperar una imagen, se lee el array de bytes y se transforma otra vez en imagen para visualizarla por pantalla, imprimirla, o lo que sea…

Redimensionar imagenes

Cuál es el problema entonces? Existen multitud de ejemplos en Internet acerca de cómo redimensionar imágenes:

public static Image ResizeImage(this Image oldImage, int targetSize)

{

    Size newSize = calculateDimensions(oldImage.Size, targetSize);

    using (Bitmap newImage = new Bitmap(newSize.Width, 

        newSize.Height, PixelFormat.Format24bppRgb))

    {

        using (Graphics canvas = Graphics.FromImage(newImage))

        {

            canvas.SmoothingMode = SmoothingMode.AntiAlias;

            canvas.InterpolationMode = InterpolationMode.HighQualityBicubic;

            canvas.PixelOffsetMode = PixelOffsetMode.HighQuality;

            canvas.DrawImage(oldImage, new Rectangle(new Point(0, 0), newSize));

            using (MemoryStream m = new MemoryStream())

            {

                newImage.Save(m, ImageFormat.Jpeg);

                return (Image)newImage.Clone();

            }

        }

    }

}

El código anterior funciona bien en casi todos los casos, pero no cuando la imagen a redimensionar contiene partes transparentes, ya que las partes transparentes aparecen en negro. Esto es así porque la información de transparencia de una imagen se almacena en el canal alfa, y en el código anterior al crear el nuevo Bitmap estamos usando explícitamente el valor ‘Format24bppRgb’ de la enumeración PixelFormat, que almacena 8 bits para cada color primario.

images_fail

En su lugar, debemos utilizar el valor ‘Format32bppRgb’ que almacena 8 bits para cada color primario más 8 bits para el canal alfa. También podemos omitir el formato en el constructor y pasar sólo el ancho y alto, ya que por defecto se usará el valor ‘Format32bppRgb’ en caso que no sea suministrado.

De todos modos, el código anterior es sólo a efectos de ilustrar el ejemplo, ya que para redimensionar una imagen es mucho más sencillo usar el método ‘GetThumbnailImage’ de la clase ‘Image’:

public static Image ResizeImage(this Image oldImage, int targetSize)

{

    Size newSize = calculateDimensions(oldImage.Size, targetSize);

    return oldImage.GetThumbnailImage(newSize.Width, newSize.Height, () => false, IntPtr.Zero); 

}

Convirtiendo imágenes a bytes y viceversa

También existen multitud de ejemplos acerca de convertir imágenes a matrices y a la inversa. Veamos algunos ejemplos:

1) Mediante un MemoryStream: en este ejemplo se vuelca la imagen en un stream en memoria, y posteriormente se transforma en un array.

public static byte[] ConvertImageToByteArray(System.Drawing.Image imageIn)

{

    using (System.IO.MemoryStream ms = new System.IO.MemoryStream())

    {

        imageIn.Save(ms, ImageFormat.Jpeg);

        return ms.ToArray();

    }

}

 

public static Image ConvertByteArrayToImage(byte[] byteArrayIn)

{

    using (System.IO.MemoryStream ms = new System.IO.MemoryStream(byteArrayIn))

    {

        Image returnImage = Image.FromStream(ms);

        return returnImage;

    }

}

Resultaría muy sencillo si no fuese porque al convertir la imagen a un array volvemos a tener el problema de las transparencias.

2) Otro método es utilizar código unsafe para copiar literalmente los bits de la imagen a un array:

private unsafe byte[] BmpToBytes_Unsafe(Bitmap bmp)

{

    BitmapData bData = bmp.LockBits(new Rectangle(new Point(), bmp.Size),

        ImageLockMode.ReadOnly,

        PixelFormat.Format32bppArgb);

    int byteCount = bData.Stride * bmp.Height;

    byte[] bmpBytes = new byte[byteCount];

    Marshal.Copy(bData.Scan0, bmpBytes, 0, byteCount);

    bmp.UnlockBits(bData);

    return bmpBytes;

}

 

private unsafe Bitmap BytesToBmp_Unsafe(byte[] bmpBytes, Size imageSize)

{

    Bitmap bmp = new Bitmap(imageSize.Width, imageSize.Height);

    BitmapData bData = bmp.LockBits(new Rectangle(new Point(), bmp.Size),

        ImageLockMode.WriteOnly,

        PixelFormat.Format32bppArgb);

    Marshal.Copy(bmpBytes, 0, bData.Scan0, bmpBytes.Length);

    bmp.UnlockBits(bData);

    return bmp;

}

Sin duda éste método ofrece un mayor rendimiento, y además al especificar el formato ‘Format32bppArgb’ nos soluciona el problema de las transparencias, pero resulta que nos crea otro problema: Para posteriormente poder revertir el array a imagen necesitamos conocer el tamaño de la imagen original, y eso no es demasiado práctico.

AL final la solución ha sido mucho más simple y porque no, mucho más elegante: Usando un simple TypeConverter.ConvertTo:

public static byte[] ConvertImageToByteArray(System.Drawing.Image imageIn)

{

    return (byte[])TypeDescriptor.GetConverter(imageIn).ConvertTo(imageIn, typeof(byte[]));

}

images_success

En fin, espero que si alguien ha estado en la misma situación que yo, al menos este post le resuelva un poco la vida 🙂

Saludos desde Andorra a punto de cerrar el año,

Leave a Reply

Your email address will not be published. Required fields are marked *