Redimensionando GIF’s desde PHP y II

En el primer artículo de esta serie de dos comentamos cómo escalar GIF’s con transparencias.

Recordando conceptos, lo más importante es que la transparencia es un atributo de la imagen, no del color en sí mismo, de manera que se configura un color determinado como transparente y cualquier pixel que tenga el mismo color exacto será transparente.

Veamos ahora qué ocurre si el gif es animado. Si intentamos abrir la imagen con imagecreatefromgif, nos llevaremos la sorpresa de que sólo aparece el primer frame. Ese es el comportamiento normal, ignorar el resto.

La teoría

Un GIF animado está formado por una secuencia de imágenes GIF añadiendo cierta información adicional, como el tiempo que se muestra cada frame. La trasnparencia es común a todos los frames, es decir, vuelve a ser un atributo de la imagen final. Pero esto no es todo, según investigábamos el tema nos encontramos con dos detalles adicionales que entorpecían todavía más el proceso. El primero es que no todos los frames tenían que ser del mismo tamaño, sino que cada uno podía tener un tamaño distinto y, dentro de la información adicional que comentábamos antes, tendríamos los datos de la posición (x,y) donde situar ese frame respecto al primero. Vale, ahora que lo sabemos podemos tomar medidas en el proceso final. Pero cuando estas medidas están contempladas aparece otro problema. El concepto tradicional de secuencia de gif no es tal, en realidad hay dos modos de manejar las capas, según averiguamos con el Gimp:

  • replace: es el comportamiento normal, cada frame reemplaza al anterior, es como una película.
  • combine: los frames se sobreponen unos sobre otro, de manera que, con las transparencias de cada frame, se genera una animación cambiando sólamente la parte de la imagen que cambia. El resultado es un archivo de menor peso.

Después de mil vueltas no encontramos la manera de combinar los frames, sólo podíamos utilizar replace. Llegados a este punto el problema eran los gifs animados con frames de distintos tamaños y combinados.

Antes de entrar en materia con el código, explicaremos a grandes rasgos cómo hacerlo, una vez entendamos el procedimiento será más sencillo ver el código. La solución pasa por separar el GIF animado original en sus frames, tratar cada frame por separado y volver a unirlos generando el nuevo gif animado. A la hora de tratar cada gif tenemos el problema descrito anteriormente con tamaños y replace/combine. El resultado fué generar un animado de tipo replace apartir del combine original fusionando cada frame con el inmediato anterior. Expliquemos esto más detalladamente. Según la teoría vista hasta ahora, en un GIF de tipo combine lo que se ve en cada momento es la superposición del frame actual más todos los anteriores. Es de suponer, además, que tendrán transparencias, puesto que sino uno sustituiría completamente a los otros. Por ejemplo, supongamos que tenemos un GIF de 5 frames y la reproducción va por el tercero. En ese momento estaríamos viendo el primer frame con el segundo encima, que al aplicar la transparencia dejaría ver el primero donde tengamos transparencia y encima de todo nuestro tercer frame que dejaría ver los de abajo. Una imagen vale más que mil palabras:

Ejemplo de gif animado separado en capas

En esta imagen se muestra todo lo explicado. En la fila superior vemos la animación desglosada por frames. Les he puesto fondo gris para que lo veáis, pero en realidad sería transparente. Las líneas discontínuas representan el tamaño de esa capa sobre el tamaño total de la imagen representada por el primer frame. Como podéis apreciar hay variedad en cuanto a tamaño y posiciones. En la fila de abajo he ido fusionando los frames a medida que se van viendo. El segundo frame sería la fusión del primero con el segundo de la fila de arriba, que es en su mayoría transparente y solo añade unas pinceladas sobre el diseño inicial. El tercero es la fusión del segundo con el tercero de arriba, etc. Es el efecto combine, combina todas las capas que va mostrando. El resultado de la fila de abajo es una secuencia replace, cada frame es independiente del otro. Este es nuestro objetivo, convertir una imagen inicial tipo combine en una replace.

Esta sería la imagen original para que entendáis el efecto final..

Ejemplo de gif animado

La práctica

Ahora que ya sabemos qué es lo que tenemos que hacer surge la pregunta del cómo.

Para empezar os preguntaréis cómo separamos un gif animado en sus frames y los volvemos a juntar si las funciones de PHP sólo se quedan con el primer frame. Sencillo, con otras funciones. Aquí es donde vienen a nuestra ayuda un par de estupendas clases realizadas por László Zsidi, GIFEncoder y GIFDecoder. Su función es básicamente lo que dice su nombre, coger un gif y separarlo en sus frames o coger varios frames (o imágenes sueltas en disco) y devolver un GIF animado. El cómo no nos importa, queda fuera del alcance de este artículo y puedes consultar el código de las clases si te interesa. Simplemente comentaré que lo hace todo a nivel de bits, no utiliza ninguna función gráfica predefinida.

Al final de todo tenéis un zip con todo el código fuente y las clases necesarias.

include(dirname(__FILE__)."/GIFDecoder.class.php");
include(dirname(__FILE__)."/GIFEncoder.class.php");         

$gifcontents=file_get_contents("miarchivo.gif");
if(is_gif_animado($gifcontents)){
 $GIFS=new GIFDecoder($gifcontents);
 $ARRS=$GIFS->GIFGetFrames();
 $OFFSETS=$GIFS->GIFGetOffset();
}

Este sería el principio. Es importante comprobar si es una animación, puesto que sino será todo mucho más sencillo como vimos en el primer capítulo. La función la encontraréis en el código. De aquí, lo importante que nos queda es

  • $GIFS->GifGetFrames() contiene los frames del gif
  • $GIFS->GifGetOffset() las desviaciones en la posición de cada capa.

Con esto podemos comenzar a operar sobre cada gif a través del array ARRS. El código está explicado en los comentarios. Es importante anotar que tanto el color de la transparencia como el tamaño de la imagen lo obtenemos con las funciones estandard. Esto lo hacemos así porque no encontré la manera de saber el color de transparencia a traves del GIFDecoder y, ya que tenía la imagen abierta, saco el tamaño.

//con las funciones estandard detectamos el tamaño del primer frame y el color de transparencia
$srcImage = imagecreatefromgif($ipath);
$transcolor=imagecolortransparent($srcImage);
$w=imagesx($srcImage);
$h=imagesy($srcImage);for($i=0; $i<count($ARRS); $i++){
    //el primer frame se queda como esta
    if($i>0){ 	$image=imagecreatefromstring($ARRS[$i]);
 	$wo=imagesx($image);
 	$ho=imagesy($image);
 	$off=$OFFSETS[$i];
 	//fusionamos el frame con el anterior
 	$temp=imagecreatetruecolor($w, $h);
 	$temp2=imagecreatefromstring($ARRS[$i-1]);
 	//hay que tener en cuenta la posible transparencia, sino no hacemos nada
	if($transcolor!=-1){
	 	$trnprt_color = @imagecolorsforindex($image, $transcolor);
 		$trnprt_indx = @imagecolorallocate($temp, $trnprt_color['red'], $trnprt_color['green'], $trnprt_color['blue']);
	 	if($trnprt_indx){
 			imagefill($temp, 0, 0, $trnprt_indx);
 			imagecolortransparent($temp, $trnprt_indx);
	 		imagecolortransparent($image, $transcolor);
 		}
	}          

	//creamos una imagen con el frame anterior y superponemos el actual con el offset necesario
	imagecopymerge($temp, $temp2, 0, 0, 0, 0, $w, $h, 100);
	imagecopymerge($temp, $image, $off[0], $off[1], 0, 0, $wo, $ho, 100);       

 	//capturamos el nuevo frame
	ob_start();
	imagegif($temp);
	$GIFS->ARRS[$i]=ob_get_clean();         

	//ya no hay offsets puesto que todos tienen el mismo tamaño
	$GIFS->offsets[$i][0]=0;
	$GIFS->offsets[$i][1]=0;
	imagedestroy($temp);
    }
}

En este punto tenemos exactamente el proceso que hemos estado explicando. Todos los frames tienen el mismo tamaño y son independientes, es decir, se han ido fusionando hacia arriba de manera que podemos hacer una animación replaced. Interesante es la manera de recuperar el gif de cada frame modificado para restaurarlo al array original. No tenemos otra forma de recuperar el contenido de una imagen que no sea mostrándola o guardándola a un archivo, así que la mostramos y la recuperamos del buffer de salida.

Ahora podemos hacer lo que necesitemos con la imagen, escalarla, imprimir texto, rotarla, etc. Nosotros la escalaremos, haremos un thumbnail de 90×90. Tan sencillo como hacer el proceso en todos y cada uno de los frames.

//ahora si queremos podemos redimensionar los frames, por ejemplo hacemos thumbnail
for($i=0; $i<count($ARRS); $i++){
 	$image=imagecreatefromstring($ARRS[$i]);
 	$wo=imagesx($image);
 	$ho=imagesy($image);
 	$dst_img=imagecreatetruecolor (90, 90);
 	imagecopyresized($dst_img, $image, 0, 0, 0, 0, 90, 90, $wo, $ho);
	ob_start();
	imagegif($dst_img);
	$ARRS[$i]=ob_get_clean();
}

Para mostrar la imagen, se necesite o no guardarla en disco, es necesario pasarla a un archivo, pues es el proceso que hace la clase GIFEncoder. Si después no la necesitamos se puede eliminar directamente como hacemos en el ejemplo.

$gif = new GIFEncoder($GIFS->ARRS, $GIFS->GIFS->GIFGetDelays(), 0, 2, $trnprt_color['red'], $trnprt_color['green'], $trnprt_color['blue'], array(), "bin" );
$fh = fopen("final.gif", 'w');
fwrite($fh, $gif->GetAnimation());
fclose($fh);
header("Content-type: image/gif");
readfile("final.gif");
unlink("final.gif");

Os dejo el enlace con el código fuente y el ejemplo funcionando.

Como véis el proceso es bastante complejo, pero una vez se entiende el funcionamiento del GIF animado todo es más sencillo. Como comentario adicional, si váis a implementar un sistema de escalado en tiempo real, yo pensaría en cachear los resultados por aquello de optimizar un poco el sistema y no sobrecargar la cpu, después de todo estamos hablando de trabajo con imágenes, algo bastante pesado en memoria.

Finalmente, utilizaba inicialmente la función de comprobación de que una imagen es animada de László Zsidi, sin embargo detectamos que, en determinadas situaciones, entraba en un bucle infinito y terminaba saturando nuestro Apache de procesos ocupados en el bucle infinito. Menos mal que tenía otra versión más antigua de una función que hacía lo mismo. Deuteros me lo agradece enormemente.

10 comentarios en “Redimensionando GIF’s desde PHP y II

  1. Hola Tuixx,

    Lo tienes justo debajo del último trozo de código:
    “Os dejo el enlace con el código fuente y el ejemplo funcionando.”

    Un saludo

  2. Gracias. Uso GifEncoder. El Gif Animado resultante es bastante grande (30 Megas) porque es atípico, no obstante los browsers requiren de 300 a 400 Megas para mostrarlo!!. Esto lo hago en php en una máquina fedora. Si obtengo el Gif Animado con alguna herramienta en Windows el Gif animado resultante tambien es de 30 Megas PERO los browsers consumen igual memoria para mostrarlo. (??) Que es lo que me falta o el error en GifEncoder para php en linux ??? Debo agregar alguna función u instruccion para optimizar la memoria ???
    Gracias por cualquier ayuda.

  3. Buenas Ocl!!

    Hombre, por lo que dices, el consumo de memoria no está relacionado con la forma de generar el gif, sino por el gif en sí mismo. Piensa que un gif de 30mb es un archivo comprimido que pesa 30mb, al descomprimir cada frame para representarlo el tamaño aumenta considerablemente.
    Creo que tu problema es que quieres utilizar un gif para algo que no debe ser el formato adecuado, un gif de 30mb me parece una animalada.
    Ojo, es mi opinión. Lo único que tengo claro es que da igual el procedimiento, el problema es el archivo en sí mismo.

  4. Bueno, lo que sucede es que el Gif es el acumulado de imagenes generadas por una herramienta, la cual toma instantaneas cada 5 minutos de una gran red, las 24 horas. Cada gráfica pesa alrededor de 100K pero son 288 frames y por eso resulta el bicho en 30 Megas. Lo que queremos es ver el comportamiento de tráfico de todo un día en solo un minuto (no es para pegarlo a ninguna página web, lo que si sería una “re-animalada” (nos reimos por lo de “animalada”). )
    De todas maneras el “engendro” trabaja bien en maquinas con buena Ram, pero creo que ud. tiene toda la razón y debe lograrse lo mismo con otras herramientas. Quiza volviendolo a video, no sé, o simulando un Gif Animado sin serlo a partir de los frames individuales. Investigaré.
    Muchísimas gracias por su respuesta. Saludos.

  5. Se me ocurre que después de generar el gif (por el método que prefieras) lo conviertas a vídeo. ffmpeg es una herramienta ideal para esto pues trabaja el línea de comandos. Así puedes pasarlo al formato que deseas, incluso si lo que necesitas es verlo online en un navegador puedes pasarlo a Flash Video y visualizarlo con una pequeña aplicación Flash, así no dependerás de reproductores externos y tendrás todo bien integrado.

  6. Ok. Voy a a probarlo. Aproveché para ojear su página y tiene varios artículos interesantes, no solo lo de Gifs. Gracias por compartir el conocimiento. Saludos desde Colombia.

Deja un comentario

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