Alternativamente, si no es posible usar otras extensiones, podría manipularse el GIF directamente, redimensionando cada cuadro uno por uno usando una de las funciones que trabajan con imágenes estáticas, como imagecopyresampled(). El formato de un GIF animado no es demasiado complicado, así que con un poco de ayuda de la especificación{1} y de un programa decodificador de ejemplo{2}, he escrito un pequeño ejemplo de cómo podría implementarse esto:
Código PHP:
<?php
class GifInfo
{
// Gestor de lectura de archivo
var $_gestor;
// Matriz con secuencias de bytes que componen el archivo GIF
var $_datos;
// Matriz que indica cuáles índices en $datos corresponden a imágenes
var $_imagenes;
// Tabla global de colores, y su especificación de tamaño
var $_tcg;
var $_tcg_tam;
function ExtraerCompleto ()
{
$bufer = '';
$n_datos = count ($this->_datos);
for ($i = 0; $i < $n_datos; $i++) {
$bufer .= $this->_datos[$i];
}
return $bufer;
}
function ExtraerImagen ($n)
{
if ($n < 1 || $n > $this->_imagenes['cantidad'])
trigger_error ("No se puede extraer la imagen número $n");
$bufer = '';
$n_datos = count ($this->_datos);
for ($i = 0; $i < $n_datos; $i++) {
if (! isset ($this->_imagenes['indices'][$i])
|| $this->_imagenes['indices'][$i] == $n)
$bufer .= $this->_datos[$i];
}
return $bufer;
}
function Leer ($archivo)
{
if ($this->_gestor !== NULL) {
fclose ($this->_gestor);
}
$this->_gestor = fopen ($archivo, 'r');
$this->_datos = array ();
$this->_imagenes = array (
'cantidad' => 0,
'cabeceras' => array (),
'indices' => array ());
// Cabecera
$this->_datos[] = fread ($this->_gestor, 6);
// Descriptor lógico de pantalla
$this->_datos[] = fread ($this->_gestor, 7);
$dlp = $this->dato_reciente ();
$this->_tcg = '';
$this->_tcg_tam = 0;
if ($this->byte ($dlp, 5) & 0x80 != 0) {
$n = 2 << ($this->byte ($dlp, 5) & 7);
$this->_tcg_tam = $this->byte ($dlp, 5) & 7;
// Tabla global de colores
$this->_tcg = fread ($this->_gestor, 3 * $n);
$this->_datos[] = $this->_tcg;
}
$fin = FALSE;
while (! $fin) {
$cod = $this->leer_byte ();
$pos = $this->dato_reciente ();
switch ($cod) {
case 0x2c:
$cab = $this->leer_imagen ();
$this->agregar_imagen ($pos, $this->dato_reciente (), $cab);
break;
case 0x21:
$this->leer_extension ();
break;
case 0x3b:
$fin = TRUE;
break;
case 0x00:
break;
default:
trigger_error ('Byte inválido en la imagen GIF');
}
}
}
function NumImagenes ()
{
return $this->_imagenes['cantidad'];
}
function ReemplazarImagen ($n, $archivo_gif)
{
$otro_gif = new GifInfo ();
$otro_gif->Leer ($archivo_gif);
$n_datos = count ($this->_datos);
$bufer = '';
for ($i = 0; $i < $n_datos; $i++) {
if (isset ($this->_imagenes['indices'][$i])
&& $this->_imagenes['indices'][$i] == $n)
break;
$bufer .= $this->_datos[$i];
}
while (isset ($this->_imagenes['indices'][$i])
&& $this->_imagenes['indices'][$i] == $n)
$i++;
$otro_n = count ($otro_gif->_datos);
$otro_cab = $otro_gif->_imagenes['cabeceras'][1];
for ($j = 0; $j < $otro_n; $j++) {
if (isset ($otro_gif->_imagenes['indices'][$j])
&& $otro_gif->_imagenes['indices'][$j] == 1) {
// Si la imagen no tiene tabla de colores local, usar la
// global
if ($j == $otro_cab
&& ($otro_gif->byte ($otro_cab, 9) & 0x80) == 0) {
$byte = $otro_gif->byte ($otro_cab, 9);
$byte |= 0x80;
$byte &= 0xF8;
$byte |= $otro_gif->_tcg_tam;
$bufer .= substr_replace ($otro_gif->_datos[$j],
chr ($byte), 8, 1);
$bufer .= $otro_gif->_tcg;
continue;
}
$bufer .= $otro_gif->_datos[$j];
}
}
for (; $i < $n_datos; $i++) {
$bufer .= $this->_datos[$i];
}
return $bufer;
}
function TamImagen ($n = 1)
{
if ($n < 1 || $n > $this->_imagenes['cantidad'])
trigger_error ('No se puede obtener el tamaño de la imagen');
$cab = $this->_imagenes['cabeceras'][$n];
$ancho = $this->byte ($cab, 5) | ($this->byte ($cab, 6) << 8);
$alto = $this->byte ($cab, 7) | ($this->byte ($cab, 8) << 8);
return array ($ancho, $alto);
}
function agregar_imagen ($ind_comienzo, $ind_final, $cabecera)
{
$this->_imagenes['cantidad']++;
$img = $this->_imagenes['cantidad'];
$this->_imagenes['cabeceras'][$img] = $cabecera;
for ($i = $ind_comienzo; $i <= $ind_final; $i++) {
$this->_imagenes['indices'][$i] = $img;
}
}
function byte ($ind_dato, $ind_byte)
{
return ord (substr ($this->_datos[$ind_dato], $ind_byte - 1, 1));
}
function dato_reciente ()
{
return count ($this->_datos) - 1;
}
function leer_bloque ()
{
$tam = $this->leer_byte ();
if ($tam > 0) {
$bloque = fread ($this->_gestor, $tam);
if (strlen ($bloque) != $tam)
trigger_error ('Error al leer bloque del archivo GIF');
$this->_datos[] = $bloque;
}
return $tam;
}
function leer_bloques ()
{
do {
$tam = $this->leer_bloque ();
} while ($tam > 0);
}
function leer_byte ()
{
$byte = fread ($this->_gestor, 1);
$this->_datos[] = $byte;
return ord ($byte);
}
function leer_extension ()
{
$cod = $this->leer_byte ();
switch ($cod) {
case 0xf9:
$this->_datos[] = fread ($this->_gestor, 6);
break;
default:
$this->leer_bloques ();
}
}
function leer_imagen ()
{
// Cabecera de imagen (posición, tamaño, info)
$this->_datos[] = fread ($this->_gestor, 9);
$cab = $this->dato_reciente ();
if ($this->byte ($cab, 9) & 0x80 != 0) {
$n = 2 << ($this->byte ($cab, 9) & 7);
$this->_datos[] = fread ($this->_gestor, $n * 3);
}
// Tamaño de código LZW mínimo
$this->leer_byte ();
$this->leer_bloques ();
return $cab;
}
}
/********************
** Ejemplo de uso **
********************/
$entrada = 'ejemplo.gif';
$salida = 'ej_reducido.gif';
$tmp = 'tmp.gif'; // archivo temporal para realizar los cambios
// Reducir al 80% del tamaño original
$conversion = 0.8;
$gif = new GifInfo ();
$gif->Leer ($entrada);
$n = $gif->NumImagenes ();
for ($i = 1; $i <= $n; $i++) {
$img = $gif->ExtraerImagen ($i);
list ($ancho, $alto) = $gif->TamImagen ($i);
$ancho_salida = $ancho * $conversion;
$alto_salida = $alto * $conversion;
$img_fuente = imagecreatefromstring ($img);
$img_destino = imagecreate ($ancho_salida, $alto_salida);
imagecopyresampled ($img_destino, $img_fuente,
0, 0, 0, 0,
$ancho_salida, $alto_salida,
$ancho, $alto);
imagegif ($img_destino, $tmp);
$img = $gif->ReemplazarImagen ($i, $tmp);
file_put_contents ($tmp, $img);
$gif->Leer ($tmp);
}
file_put_contents ($salida, $gif->ExtraerCompleto ());
?>
Quizás el detalle más delicado al redimensionar un GIF animado es el manejo de las tablas de colores. La paleta de colores de cada cuadro redimensionado suele ser diferente a la paleta de la imagen original, y por esto no pueden simplemente copiarse directamente los cuadros redimensionados, ya que entonces éstos podrían aparecer con colores extraños.
Una solución bastante razonable sería computar una nueva tabla de colores global en base a las paletas de cada cuadro redimensionado, pero todo eso ya luce más como labor para una biblioteca especializada de imágenes :). En su lugar, el ejemplo anterior copia cada cuadro con su propia paleta de colores como paleta local. El GIF final no resulta tan optimizado como podría ser, pero es algo :).
Finalmente, nota que el proceso de redimensionar un GIF animado suele ser un poco lento, especialmente si hay muchos cuadros, así que si te resulta posible, sería recomendable que hagas las conversiones usando un programa independiente, como por ejemplo ImageMagick{3}, antes de usar tus imágenes desde PHP.
{1} http://www.w3.org/Graphics/GIF/spec-gif89a.txt
{2} http://www.fmsware.com/stuff/GifDecoder.java
{3} http://www.imagemagick.org/script/index.php