Archivo de la etiqueta: animado

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.