C# Encuadrar una imagen mediante GDI+

En los foros de MSDN es frecuente leer alguna pregunta de cómo se puede encuadrar una fotografía para poder adaptarla a un determinado tamaño. A menudo las respuestas se enfocan al entorno de desarrollo, así por ejemplo, en el caso de ser en el foro de ASP.Net, la respuesta comúnmente se enfoca a la creación de un DIV que permita simular el cuadro externo, en caso de WinForms, un panel que recubra la imagen… No obstante, cuando tratamos de hacer algo así, casi siempre nos encontramos con problemas adicionales puesto que no todas las imágenes que almacenamos tienen ni la misma resolución ni la misma orientación.

Pongamos un ejemplo que seguro que entenderemos mejor los problemas. Para el ejemplo opto por usar WebForms y busco por internet tres fotografías diferentes de la Sagrada Familia de Barcelona con resoluciones y orientación diferentes: 160×230, 640×480 y 4000×3000.

Crear el entorno ASP.Net

Para simular el marco de la fotografía, lo que hago es crear un div que envolverá la fotografía

   1: <div id="showFoto">
   2:     <asp:Image ID="foto" runat="server"   />
   3: </div>

Y, lógicamente, defino los estilos:

   1: #showFoto
   2: {
   3:     background-color: #FFFF00;
   4:     border: thin inset #FFFF00;
   5:     width: 500px;
   6:     height:375px;
   7:     margin: 0 auto;
   8: }
   9:  
  10: #showFoto img
  11: {
  12:     position:relative;
  13:     top: 50%;
  14:     left: 50%;
  15:     width: 460px;
  16:     height:345px;
  17:     margin-top: -172px;
  18:     margin-left: -230px;
  19: }

Con esto un resultado más o menos óptimo en IE9, Firefox 9 y Chrome 16 siempre y cuando la fotografía tenga una orientación horizontal.

image

Eso sí, al haber hecho uso de medidas fijas tanto para el ancho como para el alto de la capa y de la foto, en el caso de que ésta última tenga una orientación vertical, podemos decir que no queda tan bonito puesto que se deforma la imagen.

image

Démosle la vuelta a la tortilla

No sé si existe algún elemento desde el HTML o los CSS que solventen este problema pero aun así, siempre nos encontraríamos con el mismo problema si acudimos a WinForms, WPF… Le daré un vuelco al planteamiento y lo que intentaré será, desde C#, añadir un marco de un tamaño variable a la imagen y crear los métodos necesarios para que se adapte una imagen redimensionada al marco. Por tanto, creo una clase con una serie de métodos públicos que permitan realizar tal acción.

image

Dos propiedades RecuadroExterior y CapasDegradado permitirán definir el tamaño de la nueva imagen y el ancho del marco respectivamente; y tres métodos sobrecargados que intentarán facilitarnos el trabajo según el origen sea un array de bytes extraído de la base de datos, una imagen o, sencillamente, la ruta completa de un fichero.

Para la conversión del array de bytes en Imagen (o si necesitamos la inversa) uso una rutina muy conocida por los foros de ASP.Net que nos lo permite hacer:

   1: /// <summary>
   2:  /// Convierte un array de bytes en una imagen
   3:  /// </summary>
   4:  /// <remarks>
   5:  /// Extraido de http://www.daniweb.com/software-development/csharp/code/365920
   6:  /// </remarks>
   7:  public static Image byteArrayToImage(byte[] byteArrayIn)
   8:  {
   9:      MemoryStream ms = new MemoryStream(byteArrayIn);
  10:      Image returnImage = Image.FromStream(ms);
  11:      return returnImage;
  12:  }
  13:  
  14:  /// <summary>
  15:  /// Convierte una imagen en un array de bytes
  16:  /// </summary>
  17:  /// /// <remarks>
  18:  /// Extraido de http://www.daniweb.com/software-development/csharp/code/365920
  19:  /// </remarks>
  20:  public static byte[] imageToByteArray(System.Drawing.Image imageIn)
  21:  {
  22:      MemoryStream ms = new MemoryStream();
  23:      imageIn.Save(ms, System.Drawing.Imaging.ImageFormat.Gif);
  24:      return ms.ToArray();
  25:  }

Para las propiedades de los tamaños abogo por establecer unos valores más o menos estándares sin olvidar que podemos modificarlos en cada momento:

   1: /// <summary>
   2: /// Obtiene o establece el tamaño del cuadro de salida
   3: /// Por defecto: 640 x 480
   4: /// </summary>
   5: public Size RecuadroExterior
   6: {
   7:     get { return _recuadroExterior; }
   8:     set { _recuadroExterior = value; }
   9: }
  10:  
  11: /// <summary>
  12: /// Obtiene o establece las capas del marco, es decir, su ancho en 
  13: /// píxeles
  14: /// </summary>
  15: public int CapasDegradado
  16: {
  17:     get { return _capasDegradado; }
  18:     set { _capasDegradado = value; }
  19: }
  20:  
  21: private Size _recuadroExterior = new Size(640, 480);
  22: private int _capasDegradado = 20;

Una vez creado más o menos el entorno, nos falta definir la metodología a seguir que, más o menos, será la siguiente:

  1. Lectura de la imagen inicial
  2. Creación de una nueva imagen con el tamaño definido en RecuadroExterior
  3. Creación del marco
  4. Comparar el tamaño de la imagen inicial con el tamaño del marco.
    1. Si la imagen es mayor que el marco, se redimensionará adaptándola al extremo más amplio
  5. Se centrará la imagen tanto horizontal como verticalmente.

Los métodos de trabajo.

Pasaremos ahora a la implementación de los métodos y comenzaremos por los métodos sobrecargados que devolverán la imagen:

   1: /// <summary>
   2:  /// Obtiene una imagen en base al parámetro <para>fichero</para>
   3:  /// </summary>
   4:  /// <param name="fichero">
   5:  /// Ruta completa del fichero gráfico que se quiere encuadrar
   6:  /// </param>
   7:  /// <returns>
   8:  /// Imagen encuadrada y sin distorsionar con el mayor tamaño posible
   9:  /// </returns>
  10:  public Image GetImagenConMarco(string fichero)
  11:  {
  12:      Image img = Image.FromFile(fichero);
  13:      return GetImagenConMarco(img);
  14:  }
  15:  
  16:  
  17:  /// <summary>
  18:  /// Obtiene una imagen encuadrada con el array de bytes pasado
  19:  /// como parámetro
  20:  /// </summary>
  21:  /// <param name="bytes">
  22:  /// Array de bytes que representa una imagen
  23:  /// </param>
  24:  /// <returns>
  25:  /// Imagen encuadrada y sin distorsionar con el mayor tamaño posible
  26:  /// </returns>
  27:  /// <remarks>Se crea este método para facilitar el trabajo
  28:  /// al extraer imágenes de una base de datos.</remarks>
  29:  public Image GetImagenConMarco(byte[] bytes)
  30:  {
  31:      Image img = JnSoftware.Utiles.GestionImagenes.byteArrayToImage(bytes);
  32:      return GetImagenConMarco(img);
  33:  }
  34:  
  35:  
  36:  /// <summary>
  37:  /// Encuadra la imagen dentro de un marco
  38:  /// </summary>
  39:  /// <param name="imagen">Imagen que quiere encuadrarse</param>
  40:  /// <returns>
  41:  /// Imagen encuadrada y sin distorsionar con el mayor tamaño posible
  42:  /// </returns>
  43:  public Image GetImagenConMarco(Image imagen)
  44:  {
  45:      imagenOriginal = imagen;
  46:      adaptaImagenACuadro();
  47:      return fondo;
  48:  }
  49:  
  50:  private Image imagenOriginal;

 

Se puede apreciar cómo el único método que realmente hace algo con respecto de la imagen es el último y, sencillamente, queda delegado al método privado adaptaImagenACuadro().

   1: /// <summary>
   2: /// Gestor de acciones.
   3: /// </summary>
   4: private void adaptaImagenACuadro()
   5: {
   6:     creaEntornoTrabajo();
   7:     estableceFondo();
   8:     creaMarco();
   9:     insertaImagen();
  10: }

Crear una nueva imagen como fondo

Creación de una nueva imagen con el tamaño especificado a la vez que la dejamos preparada para poder trabajar con GDI+.

   1: /// <summary>
   2:  /// Crea el fondo con el tamaño indicado.
   3:  /// </summary>
   4:  private void creaEntornoTrabajo()
   5:  {
   6:      // Creamos el fondo
   7:      fondo = new Bitmap(RecuadroExterior.Width, RecuadroExterior.Height);
   8:      entorno = Graphics.FromImage(fondo);
   9:  }
  10:  
  11:  private Image fondo;
  12:  Graphics entorno;

Rellenamos el fondo de color blanco que, aunque en el ejemplo es fijo, se puede cambiar fácilmente. La metodología: dibujar un rectángulo lleno que cubra toda la superficie.

   1: /// <summary>
   2: /// Crea el fondo de la imagen
   3: /// </summary>
   4: private void estableceFondo()
   5: {
   6:     Point posInicial = new Point(0, 0);
   7:     Brush colorFondo = Brushes.WhiteSmoke;
   8:  
   9:     Rectangle r = new Rectangle(posInicial, RecuadroExterior);
  10:     entorno.FillRectangle(colorFondo, r);
  11: }

Para el recuadro exterior optamos por usar la misma técnica, es decir, crear tantos recuadros no llenos en los que cada uno de ellos tiene un borde de color diferente que va desde oscuro a claro. De esta manera obtendremos un efecto de degradado.

   1: /// <summary>
   2: /// Crea un marco degradado en base a rectángulos.
   3: /// </summary>
   4: private void creaMarco()
   5: {
   6:     // Añadimos el marco
   7:     int intervalo = 255 / CapasDegradado;
   8:     for (int i = 0; i < CapasDegradado; i++)
   9:     {
  10:         Rectangle r = new Rectangle();
  11:         r.X = i;
  12:         r.Y = i;
  13:         r.Width = RecuadroExterior.Width - (i * 2);
  14:         r.Height = RecuadroExterior.Height - (i * 2);
  15:  
  16:         Pen p = new Pen(Color.FromArgb(i * intervalo, i * intervalo, i * intervalo));
  17:         entorno.DrawRectangle(p, r);
  18:     }
  19: }

Tratamiento de la imagen original

Tal como comentaba al principio, en el caso que la imagen pasada como parámetro sea inferior al recuadro, tan sólo será cuestión de centrarla en esta nueva imagen que nos hemos creado como fondo.

   1: /// <summary>
   2: /// Redimensiona la imagen y la centra
   3: /// </summary>
   4: private void insertaImagen()
   5: {
   6:     Size tamanoImagen = imagenOriginal.Size;
   7:  
   8:     // Se comprueba si la imagen es menor que el marco
   9:     if (!((tamanoImagen.Width < RecuadroExterior.Width - CapasDegradado * 2)
  10:      && (tamanoImagen.Height < RecuadroExterior.Height - CapasDegradado * 2)))
  11:     {
  12:         redimensionaImagen();
  13:     }
  14:     centraImagen();
  15: }
  16:  
  17:  
  18: // <summary>
  19: /// La imagen ya está redimensionada a un tamaño más pequeño 
  20: /// que el cuadro. 
  21: /// Tan sólo se trata de centrarla.
  22: /// </summary>
  23: private void centraImagen()
  24: {
  25:     Point posInicial = new Point();
  26:     posInicial.X = (RecuadroExterior.Width - imagenOriginal.Width) / 2;
  27:     posInicial.Y = (RecuadroExterior.Height - imagenOriginal.Height) / 2;
  28:  
  29:     Rectangle r = new Rectangle(posInicial, imagenOriginal.Size);
  30:     entorno.DrawImage(imagenOriginal, r);
  31: }

Hasta el momento, la clase será operativa en el caso que la imagen sea inferior en tamaño al recuadro deseado. Por lo tanto, tan sólo nos quedará redimensionar la imagen en el caso que tenga un tamaño superior. Para que el redimensionado sea lo más efectivo posible lo que hago es mirar la proporción ancho-alto de la imagen y comparar la longitud que sea superior. De esta manera, obtendremos el mayor tamaño posible sin que se deforme la misma.

   1: /// <summary>
   2: /// La imagen es mayor que el cuadro de presentación, así que se 
   3: /// se redimensiona para que se adapte, sin deformarse al ancho/alto 
   4: /// mayor
   5: /// </summary>
   6: private void redimensionaImagen()
   7: {
   8:     double proporcionAncho;
   9:     double proporcionAlto;
  10:     double factorReduccion;
  11:     Size s = imagenOriginal.Size;
  12:  
  13:     proporcionAncho = (double)imagenOriginal.Width / (double)(RecuadroExterior.Width - CapasDegradado * 2);
  14:     proporcionAlto = (double)imagenOriginal.Height / (double)(RecuadroExterior.Height - CapasDegradado * 2);
  15:  
  16:     if (proporcionAncho > proporcionAlto)
  17:         factorReduccion = proporcionAncho;
  18:     else
  19:         factorReduccion = proporcionAlto;
  20:  
  21:     imagenOriginal = new Bitmap(imagenOriginal, (int)(s.Width / factorReduccion), (int)(s.Height / factorReduccion));
  22: }

 

Por si acaso se ha quedado algo en el tintero a la hora de hacer los Copy&Paste del código dejo un ejemplo que muestra la funcionalidad en un entorno WinForms.

Enlace

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *