Archivo de la categoría: Programación

Movilizando Joomla

Hace unos días me llegó desde la lista de correo de dev.mobi un interesante artículo sobre la creación de sitios para móviles con Joomla.

No es que me encante Joomla, de hecho probablemente haga un artículo crítico al respecto, pero creo que es muy interesante para determinados websites por su aparente sencillez. El problema que tenía es que quería una solución que me permitiese, a partir de un portal web, tener acceso desde el móvil a una versión adaptada. Gracias a este artículo encontré este plugin que prácticamente te lo da todo hecho.

Hay algo, sin embargo, que no tiene en cuenta el plugin: las imágenes. Las imágenes que insertas en tus artículos serán, normalmente, de un tamaño bastante elevado, sobre todo si hablamos de terminales móviles donde la media es de 174px de ancho de pantalla. La solución pasa por toquetear un poco el plugin haciendo que las imágenes de los articulos pasen a través de un redimensionador automático que crearemos nosotros. Os recomiendo los artículos que escribí acerca del escalado de gifs transparentes y animados (aquí y aquí) para tener una idea de como hacerlo. La solución llega en dos pasos

1) Parcheando el plugin

Este es el cambio que debemos hacer en el archivo pdabot.php del plugin. Busca al final de todo la función onAfterRenderer y haz que quede asi:

function onAfterRender()
{
	global $mainframe;
	//DESDE AQUI
	if($GLOBALS['ispda']==true){
		$body = JResponse::getBody();
		$body = preg_replace( '/<img src="(.*)"(.*)>/i', '<img src="/redimensionar.phtml?imagen=\1">', $body );
		$body = preg_replace( '/<img(.*)width="(.*)"(.*)>/i', '<img\1\3>', $body );
		$body = preg_replace( '/<img(.*)height="(.*)"(.*)>/i', '<img\1\3>', $body );
		JResponse::setBody($body);
	}
	//HASTA AQUI

Como ves, sustituimos el atributo src de todas las imágenes por nuestro script de autoescalado redimensionar.php al que le pasamos la url original de la imagen. Además aprovechamos para quitar los atributos de width y height porque en este punto no sabemos cuales serán los resultantes, si los dejásemos se vería a su tamaño original. Hemos terminado con el plugin.

2) Escalando las imágenes

Si has entendido todo el proceso hasta aquí estarás pensando, vale, pero nos falta un dato: ¿a qué tamaño escalamos las imágenes?. En efecto, no lo sabemos… todavía.

Y aquí viene W urfl en nuestra ayuda. Wurfl es una base de datos de características de terminales móviles que te permiten conocer datos como el ancho de pantalla simplemente pasándole el UserAgent del mismo. No voy a explicar el funcionamiento de Wurfl puesto que se sale del alcance de este artículo, pero en su web tienes todo lo necesario.

Crearemos entonces nuestro redimensionar.php donde simplemente buscamos mediante Wurfl el ancho de pantalla del terminal cliente y reescalamos la imagen al tamaño adecuado. Es recomendable no hacerlo al 100% para evitar que los scrolls verticales reduzcan el espacio útil y nos aparezca también el scroll horizontal (suele pasar en los Nokia). Yo suelo descontar 10px al ancho. Para imágenes de artículos puedes aplicar un escalado porcentual, pueden no quedar bien imágenes muy grandes, déjalas al 70% por ejemplo.

Conclusiones

Rápidamente y de un modo sencillo hemos adaptado para terminales móviles tu portal en Joomla, no se puede pedir más. No olvides diseñar la plantilla pda a tu gusto y necesidades.

En webs móviles es habitual utilizar una imagen como cabecera del portal. Puedes utilizar también tu redimensionador para adaptarla automáticamente al ancho de pantalla de los clientes.

Se podrían hacer muchas más cosas si integramos Wurfl directamente en el plugin, incluso podríamos hacer que el código generado estuviese adaptado a las capacidades del terminal del cliente (xHTML, iMode, WML) creando plantillas para cada lenguaje distinto. ¿Te atreves a hacerlo tu?

Flash Player, sockets y políticas de seguridad

El pasado miércoles 9 de abril Adobe liberó una actualización de su Flash Player numerada como 9.0.124. Esta noticia pasaría completamente desapercibida si no fuese por las implicaciones que tiene. De hecho no entiendo como una actualización con los cambios que conlleva se ha numerado como una actualización menor y no se le ha dado más importancia.

El cambio fundamental de esta nueva versión radica en el modelo seguridad de la máquina virtual Flash al de acceder a sockets remotos y al utilizar headers HTTP. Recordemos que la opción de abrir sockets binarios y conectarse a ellos permitiendo aplicaciones bajo cualquier protocolo apareció en julio de 2006 junto a la versión 9 de Flash Player. Hasta ahora el modelo de seguridad de sockets era el mismo que para cualquier llamada remota bajo Flash, el famoso crossdomain.xml. Según esto, para acceder a cualquier archivo que no resida bajo el mismo host que sirve el swf que hace la llamada, el host servidor debe autorizar a la maquina virtual Flash la carga de ese archivo a través de ese crossdomain.xml, si no existe o no se autoriza el acceso, no se podrá acceder. Este es, por tanto, un archivo de políticas de seguridad y decide quién puede conectarse con Flash a tu servidor. A mi, personalmente, siempre me ha parecido algo ridículo, es como si para cargar un archivo de cualquier host desde tu navegador tuviesen que autorizártelo, pero siempre habrá quien encuentre ventajas en la seguridad. Si el archivo está publicado en Internet se supone que ya estas autorizado a leerlo, ¿para qué complicar más todo el proceso requiriendo más políticas de seguridad?. Y vaya si se complica…

Hasta ahora los sockets se trataban como archivos en lo que a políticas de seguridad se refiere, es decir, el crossdomain.xml del host al que conectábamos nos autorizaba el acceso igual que con cualquier llamada http. Si tienes una aplicación que utiliza sockets y actualizas a Flash Player 9.0.124 verás que ahora no te funciona, te salta una excepción de seguridad. Los chicos de Adobe se han inventado un sistema de políticas de seguridad exclusivo para sockets e independiente de las llamadas http.

Desde Adobe se han currado una ayuda/explicación de 7 páginas. Ninguna pega a no ser porque llegas al final de la lectura con la pregunta, vale, pero ¿qué tengo que hacer para que funcione? Es decir, mucha lectura pero poca, muy poca ayuda, da la impresión de que repiten lo mismo en todas las páginas sin aclarar nada.

La solución es sencilla a la par que curiosa y complicada para muchas aplicaciones. Debes crear una pequeña aplicación que, bajo el puerto 843 por defecto (aunque puedes usar otros puertos), debe responder a las peticiones de acceso con el crossdomain.xml. La mayoría de la gente ha entendido que debes tener tu servidor web a la escucha en el puerto 843 y devolver el crossomain.xml desde allí (como veis no está clara la documentación), pero NO es eso, no es una llamada HTTP la que te va a hacer el swf para pedir autorización. Veámoslo con un ejemplo práctico. Supongamos que fuese una solicitud HTTP, a grosso modo haríamos:

telnet www.tuhost.com 843
GET /crossdomain.xml HTP/1.0
HTTP/1.x 200 OK
Date: Tue, 15 Apr 2008 17:24:26 GMT
Server: Apache
Last-Modified: Thu, 28 Oct 2004 21:57:25 GMT
Content-Length: 128
Content-Type: text/xml

<xml version="1.0" encoding="UTF-8">.....</xml>

El xml final sería nuestro crossdomain.xml. Esto sería lo mismo que si cargamos el crossdomain.xml desde el puerto 80. Vale, pues esto es lo que NO va a hacer el swf, por tanto, no sirve. Esto es lo que va a hacer:

telnet www.tuhost.com 843
<policy-file-request/>
<xml version="1.0" encoding="UTF-8">.....</xml>

La diferencia es evidente, en el segundo caso, al contectar con el servidor simplemente le decimos <policy-file-request/> y nos debe devolver el xml del crossdomain.xml. No es el protocolo HTTP, es uno inventado que sólo responde a una petición.

El puerto no tiene porqué ser el 843, puedes modificarlo, pero Adobe pretende que 843 sea el estándar y, de hecho, está solicitando su homologación con el IANA.

Desde Adobe incluso se han puesto cachondos y te dicen que lo ideal es modificar tu aplicación en el servidor para que acepte la llamada <policy-file-request/> y responda con el XML, así no necesitarás una aplicación adicional. Pues nada chicos, a modificar todos los servidores smtp, pop, irc (nuestro caso), ftp… para que acepten la llamada. ¡Qué ridículo!

Como soy un tío enrollado os paso la solución completa. En esta URL tenéis un ejemplo de servidor en TCL/TK que funciona a la perfección y no consume recursos. Configura el puerto y la ruta al crossdomain.xml y a funcionar. Recuerda que si el puerto es inferior al 1024 deberá ser un usuario privilegiado quién ejecute la aplicación, deberías, entonces, enjaularla (chroot) para evitar posibles puertas. En realidad lo que hacemos es responder con el crossdomain.xml a cualquier petición que nos hagan, sea la correcta o no, ¿qué mas da? No vamos a hacer absolutamente nada más con ese puerto. Tendréis que preocuparos también de que se quede residente como demonio en vuestro servidor y, además, habrá que comprobar periódicamente que sigue a la escucha. Para esto recomiendo hacer un script que haga llamadas reales y espere el código XML necesario, comprobar simplemente con un netstat que el servidor ocupa el puerto no nos salvará de que el servidor se quede zombie. Por supuesto no olvides abrir tu firewall a ese puerto.

Actualización.
Acabo de terminar el artículo y a través de flexcoders me llega este enlace, mucho más claro y sencillo que la ayuda inicial. Ah! y con ejemplos de servidores en Perl y Python. En líneas generals viene a contar lo mismo que acabo de contar yo :P.

Cobrando por servicios a través de PayPal con PHP

Hace un tiempo necesitábamos crear un sistema de compras a través de PayPal con la particularidad de que teníamos recibir confirmación instantánea del pago para reflejar esas compras al usuario. Esta era la diferencia principal respecto de un carro de compra de una web donde vendes algo físicamente, en este caso no necesitas saber el estado de una compra, cuando el equipo de almacén prepare el pedido ya comprobará si se ha realizado el pago antes de servirlo. Nuestro caso es más complicado. Vendemos servicios que pueden ser suscripciones, créditos de uso en la web, servicios premium, acceso a zonas privadas… todo este tipo de opciones donde el usuario, después de pagar, vuelve a tu web para disfrutar de los servicios que ha comprado.

Al querer cobrar a través de PayPal tienes dos opciones, o creas botones estáticos (Comprar Ahora) desde la web de PayPal o generas los tuyos propios. El problema de crearlos estáticos es que debes crear uno por cada usuario y para cada servicio que necesites vender, algo absurdo para el caso que tratamos ya que estarías ante miles de botones. Al generar los tuyos propios puedes hacerlos normales o seguros, para que no se puedan modificar por el camino. Este es el método que deberías utilizar normalmente ya que, en los normales, un usuario experimentado puede modificar el importe a cobrar, algo que no deseas que ocurra.

Para que el sistema funcionase bien deberíamos recibir en una URL las compras realizadas de alguna manera que nos permitiese identificar al usuario que habia hecho la compra para añadirle los servicios en cuestión. La solución es crear botones firmados online utilizando como identificador de producto de compra el idUsuario de tu base de datos, de esta forma las confirmaciones de compra te indican el usuario al que pertenecen y ya puedas darle los servicios por los que ha pagado. Puedes utilizar combinaciones más elaboradas para reutilizar el sistema para distintos productos, por ejemplo idUsuario-idServicio (12345-3). Al recibirlo, con dividir la cadena por el guión ya tienes todo lo necesario. En nuestro caso había varios tipos de servicio pero se indentificaban por el coste del mismo, con lo cual con el identificador de usuario teníamos suficiente.

Aparentemente es sencillo el proceso, pero se complica ya que la documentación, a pesar de ser extensa, no esta del todo claro. Por un lado no está nada claro cómo configurar tu cuenta de PayPal para tener el sistema completo y por otro es bastante lioso el modo de crear los botones y recibir las confirmaciones. Vamos a explicarlo detalladamente ya que es un interesante ejercicio de programación en PHP.

Configurando la cuenta de PayPal

Lo primero que tienes que hacer es crear un certificado público X.509, único sistema que acepta PayPal.

Las instrucciones detallas las tienes aquí. básicamente necesitas tener openssl funcionando, cualquier sistema Linux lo tendrá instalado y sino tienes versiones para Windows.

openssl genrsa -out my-prvkey.pem 1024
openssl req -new -key my-prvkey.pem -x509 -days 3650 -out my-pubcert.pem

Con esto generamos primero la clave privada y a continuación el certificado público X.509 de esa clave. Es importante el parámetro -days, lo hemos puesto a 10 años para no tener problemas. En mi caso, la primera vez puse 365 días tal como viene en el ejemplo y al año dejó de funcionar sin que te dieses cuenta, fueron los usuarios los que nos avisaron. Guarda los dos archivos, my-prvkey.pem y my-pubcert.pem pues los necesitaremos a continuación.

Ya tenemos nuestro certificado preparado. Ahora debemos subirlo a PayPal para que sepa desencriptar nuestros botones. En tu cuenta de PayPal debes ir a:

Perfil->Configuración de pago codificado

Desde ahí, por un lado descargas el certificado público de PayPal, lo necesitaremos para codificar nuestros botones, y por otro subes tu certificado público que hemos llamado my-pubcert.pem. Obtendrás un ID de certificado, apúntatelo.

Si todo ha ido bien, ahora debemos configurar la cuenta para que PayPal sólamente acepte botones firmados, de manera que nadie pueda suplantar nuestra identidad con botones a otros precios, un detalle muy importante. Vamos entonces a:

Perfil->Preferencias de pago en el sitio Web

Primero activas Transferencia de datos de pago y te copias el Código Personal de Identidad que te indica, lo necesitaremos más adelante. A continuación, en la sección Pagos en el sitio Web codificado activamos la opción Bloquear pago en el sitio Web no codificado. Como véis no estaba tan claro el proceso.

Este paso no es obligatorio, pero haciéndolo conseguimos que el usuario vuelva a tu web una vez haya realizado el pago y, si ha sido correcto, tenga ya sus servicios disponibles. Activa la opción Retroceso automático e indica la URL de devolución, es decir, la URL donde será devuelto tu cliente, por ejemplo: http://www.tudominio.com/index.php?accion=creditos_paypalok

Finalmente activaremos la opción que nos permitirá recibir notificaciones de pagos online. Para ello vamos a:

Perfil->Preferencias de Notificación de pago instantánea

Y activaremos la notificación indicando la URL donde las vamos a recibir, por ejemplo, http://www.tudominio.com/secure/paypal/ipn.php.

Si has llegado a este punto, ya tienes todo lo necesario para comenzar con el código. Como recordatorio, necesitas:

  • Tu clave privada, my-prvkey.pem.
  • Tu certificado público, my-pubcert.pem.
  • Tu ID de certificado en PayPal
  • Tu Código Personal de Identidad
  • Certificado público de Paypal, paypal_cert_pem.txt.

Nos quedan, entonces, tres tareas pendientes:

  • Crear botones de compra
  • Crear el script de recepción de cobros realizados
  • Crear el script de vuelta después de una compra

Creando los botones

Comenzamos con el código. El único requerimiento es que tu instalación de PHP debe tener configurada la extensión openssl imprescindible para trabajar con los certificados. Con esta clase que encontré en su momento (me costó bastante localizar algo sencillo) tienes todo el proceso automatizado, sólo debes preocuparte por indicarle los datos que hemos ido guardando, los certificados y el ID del tu certificado en PayPal, simple ¿no?.

include("Class.PayPalEWP.php");
$paypal = &new PayPalEWP();
$paypal->setTempFileDirectory("/tmp");
$paypal->setCertificate("my-pubcert.pem", "my-prvkey.pem");
$paypal->setCertificateID("XXXXXXXXXX");
$paypal->setPayPalCertificate("paypal_cert_pem.txt");

$paypalParam = array(
    'cmd' => '_xclick',
    'business' => '[email protected]',
    'item_name' => 'Comprar Servicio X,
    'item_number' => $_SESSION['idUsuario'],
    'amount' => '5',
    'no_shipping' => '1',
    'currency_code' => 'EUR',
    'lc' => 'ES',
);
$form5="<form action="https://www.paypal.com/cgi-bin/webscr" method="post">
            <input type="hidden" name="cmd" value="_s-xclick"/>
            <input type="hidden" name="encrypted" value="-----BEGIN PKCS7-----n".$paypal->encryptButton($paypalParam)."n-----END PKCS7-----"/>
            <input type="image" src="imagenes/comprar_paypal.gif" border="0" name="submit" alt="Realice pagos con PayPal: es rápido, gratis y seguro." style="border:0;">
        </form>";

El código es bastante claro.
Ya lo tenemos, $form5 contiene el código de tu botón.
Los parámetros importantes son:

  • item_name: informativo, para que tu cliente sepa lo que compra. Por ejemplo: compra de suscripción a noticias.
  • item_number: Aquí configuramos el idUsuario de tu cliente en tu base de datos, así sabes quién compra.
  • amount: Precio que le cobras, en este caso 5 euros.

Modifica estos y los demás parámetros para reflejar tus opciones y adecuarlos a tu aplicación. Al sacar el código de $form5 en tu página tienes listo el sistema de compra.

Recibiendo las notificaciones

La recepción de notificaciones es la piedra angular del sistema para estar seguros de que un cliente ha pagado por un servicio. PayPal ha pensado que puedes llegar a tener problamas temporales de conectividad que te impidan reconocer una compra, con lo cual obliga a que le confirmes que has recibido la confirmación reenviándole los mismos parámetros que te ha enviado. Un sistema curioso pero efectivo, no puedes suplantarlo puesto que no sabes los identificadores de operación que te va a enviar, así que sólo el receptor de la confirmación podrá confirmar la recepción. Lo que hacemos es crear una solicitud HTTP POST con todos los parámetros que nos ha enviado y se la devolvemos a PayPal. Si todo ha ido bien recibiremos un VERIFIED y podremos aceptar esa transaccion como válida y hacer el procesado que estimemos oportuno, comenzando por comprobar la duplicidad de la transacción ya que PayPal podría estar reenviándola y terminando por otorgar al usuario el servicio por el que ha pagado.

// read the post from PayPal system and add 'cmd'
$req = 'cmd=_notify-validate';
foreach ($_POST as $key => $value) {
    $value = urlencode(stripslashes($value));
    $req .= "&$key=$value";
}

// post back to PayPal system to validate
$header .= "POST /cgi-bin/webscr HTTP/1.0rn";
$header .= "Content-Type: application/x-www-form-urlencodedrn";
$header .= "Content-Length: " . strlen($req) . "rnrn";
$fp = fsockopen ('www.paypal.com', 80, $errno, $errstr, 30);

// assign posted variables to local variables
$item_name = $_POST['item_name'];
$item_number = $_POST['item_number'];
$payment_status = $_POST['payment_status'];
$payment_amount = $_POST['mc_gross'];
$payment_currency = $_POST['mc_currency'];
$txn_id = $_POST['txn_id'];
$receiver_email = $_POST['receiver_email'];
$payer_email = $_POST['payer_email'];
$transid=$_POST['txn_id'];
$idUsuario=$_POST['item_number'];
$cantidad=$_POST['mc_gross'];
$creditos=100;

if (!$fp) {
    //CONTROL DE ERRORES; NO SE PUEDE CONECTAR CON PAYPAL
    //NO ES GRAVE, COMO NO LE CONFIRMAMOS LA TRANSACCION
    //ELLOS MISMOS LA REINTENTARÁN MÁS ADELANTE
}else{
    fputs ($fp, $header . $req);
    while (!feof($fp)) {
        $res = fgets ($fp, 1024);
        if (strcmp ($res, "VERIFIED") == 0) {
            //compruebo que no se haya procesado ya la transaccion
            $query="select * from paypal where transid='$transid' and estado=1";
            $rs=$conn->Execute($query);
            $sumar=$rs->recordcount();
            if($sumar==0){
                //LOGEAMOS TODA LA TRANSACCION
                $vars="GET: ".serialize($_GET)."rnPOST: ".serialize($_POST)."";
                $query="insert into paypal (transid, fecha, estado, variables)
                    VALUES ('$transid', now(), 1, '$vars')";
                $rs=$conn->Execute($query);

                //aquí debes hacer ahora tus operaciones
                //para conceder el servicio al usuario: $idUsuario
                //incluso comprobar que idUsuario es válido
            }else{
              //TRANSACCION DUPLICADA, NO HACEMOS NADA
            }
        }else if (strcmp ($res, "INVALID") == 0) {
            //CONTROL DE ERRORES
        }
    }
    fclose ($fp);
}

Ya hemos recibido la confirmación de pago de PayPal, la hemos guardado en nuestra base de datos y le hemos dado a nuestro cliente su servicio, este proceso será distinto para cada aplicación así que no lo explicaremos, haz el tuyo como creas oportuno. Lo que sí te recomiendo es guardar una tabla con todas las transacciones recibidas a modo de log, te servirá para buscar errores o reclamaciones de usuarios.

Cabe señalar que PayPal sólamente lanza este proceso con las transacciones correctas, aquellas se que se han cobrado correctamente, nunca con las erróneas (falta de saldo, tarjeta incorrecta, etc.).

Devolviendo al usuario a nuestra web

Una vez que el usuario ha terminado su transacción en PayPal deberíamos enviarlo de nuevo a nuestra web para que comience a utilizar el servicio por el que ha pagado. Para ello PayPal nos provee del método Retroceso automático que mandará al usuario a la URL que le hayamos especificado indicando los parámetros de la transacción, entre ellos si ha sido válida o no. PayPal, además, ha pensado en todo: no puedo devolver a un usuario a la web de origen sin antes haberle comunicado el éxito de la transacción (el paso anterior). Así el mecanismo de PayPal retrasa el reenvío del usuario unos segundos para intentar que tu servidor ya esté informado de esa transacción. Simplemente genial. Ahora utilizaremos tu código personal de identidad que hemos obtenido al configurar la cuenta en PayPal. Es el parámetro que en el código llamamos $auth_token.

El proceso es parecido a la confirmación de transacciones. Debemos comunicar a PayPal que hemos recibido al petición de vuelta del usuario y que somos nosotros quién lo hacemos, sólo así nos dará los datos de la transacción. De nuevo un método curioso. Para hacerlo, nos indica por GET el identificador de la transacción y debemos devolvérselo junto a nuestro código personal de identidad mediante otra llamada HTTP POST, de este modo nos aseguramos de que somos nosotros quienes solicitamos la información. Si todo ha sido correcto PayPal nos devuelve los datos de la transacción como respuesta a esta llamada. Otra vez genial el sistema.

¿Por qué no hacerlo como en el paso anterior? Sencillo, el método IPN es transparence al usuario, es una llamada interna que hacen los sistemas de PayPal a los tuyos, nadie va a saber que datos se envían, con lo cual puedes utilizar esos datos para confirmar la transacción. En el método de retroceso, esta URL llega al usuario, con lo que podría hacer cosas que no deseamos con los datos, con lo que no nos envían nada en la solicitud, simplemente el identificador de la transacción para que internamente nosotros pidamos los datos validándonos con el código personal.

//read the post from PayPal system and add 'cmd'
$tx_token = $_GET['tx'];
$auth_token = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
$req = 'cmd=_notify-synch';
$req .= "&tx=$tx_token&at=$auth_token";
$header .= "POST /cgi-bin/webscr HTTP/1.0rn";
$header .= "Content-Type: application/x-www-form-urlencodedrn";
$header .= "Content-Length: " . strlen($req) . "rnrn";

$fp = fsockopen ('www.paypal.com', 80, $errno, $errstr, 30);
$isError=0;
if(!$fp) {
    $isError=2;//error HTTP
}else{
    fputs ($fp, $header . $req);
    // read the body data
    $res = '';
    $headerdone = false;
    while (!feof($fp)) {
        $line = fgets ($fp, 1024);
        if (strcmp($line, "rn") == 0) {
            // read the header
            $headerdone = true;
        }else if ($headerdone){
            // header has been read. now read the contents
            $res .= $line;
        }
    }    // parse the data
    $lines = explode("n", $res);
    $keyarray = array();
    if (strcmp ($lines[0], "SUCCESS") == 0) {
        for ($i=1; $i<count($lines);$i++){
            list($key,$val) = explode("=", $lines[$i]);
            $keyarray[urldecode($key)] = urldecode($val);
        }

        $isError=0;//no error
        $nombre = $keyarray['first_name']." ".$keyarray['last_name'];
        $producto = $keyarray['item_name'];
        $amount = $keyarray['payment_gross'];
        $idUsuario=$keyarray['item_number'];
        $cantidad=0+$keyarray['mc_gross'];
        $estado=$keyarray['payment_status'];
        $transid=$keyarray['txn_id'];

        //ahora ya puedes evaluar lo que necesites de tu transacción
        //y termina informando al usuario de que todo ha ido bien y ya tiene su servicio
    }else if (strcmp ($lines[0], "FAIL") == 0) {
        $isError=1; //error de transaccion
    }
}
fclose ($fp);

La respuesta que recibimos es una respuesta HTTP estandar, con lo cual debemos ser conscientes de que vamos a recibir primero todas las cabeceras HTTP de la respuesta, después dos saltos de línea y a continuación la respuesta propiamente dicha. En una petición normal esta respuesta sería el código HTML de tu página, pero en este caso recibimos la lista de parámetros de la transacción, uno por línea y del tipo:

parámetro1=valor1
parámetro2=valor2
...

El código tiene esto en cuenta y, al recoger la respuesta, regenera la lista de parámetros/valores recibidos con lo que tenemos los datos necesarios.
La primera línea va a ser SUCESS ó FAIL, está claro el dato, una indica que la transacción ha sido válida y la otra que no. Obviamente debes informar al usuario de que la operación ha sido correcta y que ya tiene su servicio disponible.

Si has entendido bien todo lo explicado hasta ahora, verás que los sistemas de recepción de notificaciones y retroceso automático son muy similares, de hecho puedes utilizar este último para validar las transacciones de igual modo que con el primero y no necesitarías éste. Pero ¿qué ocurriría si sólo implementas el segundo y el usuario cierra la ventana del pago mientras está en esos segundos de espera antes de reenviarlo a tu web? Simplemente que el usuario no tendría su servicio disponible ya que nunca ha llegado a realizar el proceso del Retroceso automático. Para solucionarlo implementamos los dos. Con el primero aseguramos que la mayoría de transacciones válidas son procesadas y los servicios otorgados al cliente, con la segunda, además de informar al cliente del éxito o fracaso se su operación, tenemos un sistema de redundancia por si el primer procedimiento hubiese fallado. Como en ámbos tenemos el identificador de transacción, simplemente debemos comprobar que esa transacción ya existe y no hacer nada o procesarla si no existe.

Y eso es todo amigos. Con esto hemos aprendido a tener un sistema de compras de servicios seguro y dinámico en PayPal. Cómo habéis podido observar, el procedimiento es bastante complejo en cuanto a configuración y desarrollo, pero es muy interesante el estudio del resultado para tener una idea más amplia de como implementar sistemas seguros.

Modelando bases de datos con MySQL Workbench

Si hay una herramienta de diseño de bases de datos conocida dentro del software libre, ésta es, sin duda, DBDesigner. Si a estas alturas no la conoces es que no diseñas bases de datos o lo haces sobre la marcha, tu sabrás lo que haces :).

Hace algunos años MySQL compró Fabforce, la empresa que desarrollaba aquél software. No he podido encontrar la fecha exacta, pero ya en agosto de 2005 se anunciaba el lanzamiento de la primera alpha de su sucesor, MySQL Workbench. Obviamente algo tiene que haber trastocado los planes iniciales de lanzamiento puesto que en abril de 2008 estamos todavía con versiones beta, eso sí, en estado ya Release Candidate. Cabe señalar que la última versión de DBDesigner es nada más y nada menos que de 2003, y aún así se sigue utilizando.

Lo interesante de este nuevo producto es que está basado completamente en su antecesor, el equipo de desarrollo es exactamente el mismo e incluso han dejado una opción para importar diseños creados con DBDesigner.

Tal como indican en el antiguo foro del programa, fabForce mantendrá el sitio web de DBDesigner hasta que se publique la primera versión de MySQL Workbench, a partir de ese momento desaparecerá. Parece que el momento está cerca.

Mi impresión es que, a pesar de los planes que tenían para el producto después de la compra por MySQL, se antepusieron productos nuevos. Nada más ni nada menos que el conjunto de nuevas utilidades bajo entorno gráfico que aparecieron hace un par de años: MySQL Administrator, QueryBrowser y Migration Toolkit, todas ellas desarrolladas por el equipo de DBDesigner, fantásticas utilidades por cierto. Recordad que hasta entonces no había ninguna herramienta visual oficial para trabajar con MySQL, las que había eran todas de terceros, una clara desventaja dentro del sector de pequeña y mediana empresa en el que pretendían entrar después del gran éxito en entornos web.

¿Qué hacía de especial a DBDesigner?

Lo primero y fundamental, una herramienta visual para modelar bases de datos y crear diagramas e/r al más puro estilo de Oracle DBDesigner o los Diagramas de SQL Server. A esto hay que añadir las increíbles funciones de sincronización e ingeniería inversa. La primera permite reflejar automáticamente en tu diseño los cambios realizados sobre tu base de datos en el servidor y viceversa, no es necesario crear toda la base de datos de nuevo. La segunda permite leer una base de datos completa desde tu servidor y crear automáticamente el esquema. Si no los has probado no sabes lo que te pierdes.

Otra de las funcionalidades era la posibilidad de crear plugins para extender sus utilidades. Traía algunos de serie, uno de ellos, SimpleWebFront, permitía generar una aplicación CRUD completa de tu diseño en PHP sin tocar una sóla línea de código.

Finalmente debemos señalar que se podía trabajar ya en aquel momento con otros gestores de bases de datos, Oracle, MSSQL, SQLLite, ODBC…

¿Qué hay de nuevo?

Han tenido que pasar cinco años para que veamos una nueva versión de una de las mejores herramientas que han existido para MySQL. Algo nuevo tiene que aportar. Básicamente se ha mantenido la esencia de la antigua aplicación modernizando la interfaz y añadiendo algunas opciones nuevas.

La mala noticia es que saldrán dos versiones, la de comunidad, gratuita, y la comercial. La diferencia fundamental es que las opciones de sincronización e ingeniería inversa contra el servidor se han dejado para la versión de pago, pero aparece una opción en la libre que permite hacer lo mismo con archivos SQL en vez de en vivo. La mayoría de nuevas funciones se incluirán en la versión comercial, pero os aseguro que no son indispensables, hablamos distintas opciones de debug y notación.

Entre los planes de MySQL figuran crear una comunidad de desarrolladores de extensiones y plugins que permitan nuevas opciones, con lo cual no sería de extrañar que la primera en aparecer fuese una versión libre de las opciones en vivo antes comentadas.

Lo mejor es que la pruebes y obtengas tu propias conclusiones. Para mi siempre es el primer paso al preparar una nueva aplicación. Si tienes un buen modelo visual es muy sencillo preparar tus clases del modelo o decirle a alguien lo que tiene que hacer sin tener que estar revisando continuamente la estructura desde el servidor.

La versión final está al caer. Mientras tanto puedes descargar la última beta desde aquí.

Enlaces interesantes de los últimos días

Nueva API de Wurfl

Tenía pendiente desde hace unos días comentar los cambios producidos en la API de Wurfl.

La teoría

Para los que no sepan de qué estamos hablando, Wurlf es una base de datos de características de dispositivos móviles. A la hora de desarrollar sites para terminales ligeros, uno de los problemas principales es la diversidad de características distintas: tamaños de pantalla, formatos multimedia soportados, lenguajes de programación… Para solucionarlo, los proveedores necesitamos mantener bases de datos de modelos con sus características principales. Una de las opciones es mantener esa base de datos manualmente, de hecho se debe hacer para responder rápidamente a nuevos terminales. Para ello registramos los UserAgents de nuevos modelos que acceden a nuestras aplicaciones para, posteriormente, buscar sus características y añadirlos a nuestra base de datos.

Wurfl es un intento de solucionar este problema desde el software libre y colaborativo. Wurfl es, fundamentalmente, un archivo XML con la información relativa a más de 10.000 terminales distintos. Para consultar este XML la gente del proyecto ha desarrollado API’s en la mayoría de lenguajes utilizados (Java, PHP, Python, Perl…). Lo más curioso es la forma de acceder a los datos. A partir del XML se crean varios miles de archivos de manera que a la hora de buscar las características de un modelo sólo hay que buscar en un archivo. A mi personalmente no me gusta mucho el modelo. Un proyecto paralelo muy interesante es Tera-Wurfl, basándose en el XML de Wurfl crea una base de datos MySQL y modifica la API PHP para consultar los datos en ella en vez de en los miles de archivos. Nosotros hicimos en su día modificaciones sobre este proyecto para utilizarlo con Sql Server.

Wurfl lo mantiene la gente, gente como nosotros, añadiendo nuevos UserAgents. Obviamente esto no es perfecto, lleva a incongruencias y datos erróneos. Recientemente han publicado también una aplicación web para enviar nuevos UserAgents y nuevos modelos. Hemos llegado a la conclusión de que no hay un modelo único válido, sino que debe ser una combinación de Wurfl y de tu base de datos propia.

¿Existen alternavitas a Wurfl? Sí, por supuesto, alguna hay, aunque en general son de pago para uso en producción.

De DetectRight, cuando lo probamos, no me gustaba el modelo, las consultas se hacen online, añadiendo un retardo y tráfico no necesario. Device Atlas me ha parecido más interesante. Lo probamos el día que lo publicaron y no nos convenció, tenia muchas carencias de características, pero estos días lo hemos vuelto a probar y ha mejorado considerablemente, aunque al no ser gratuito, aunque sea poco, no me convence, habrá que seguirle la pista de todos modos..

¿Qúe es lo que ha cambiado entonces en la API?

El proceso por el cual Wurfl averigua a qué modelo pertenece un UserAgent es muy curioso. No se basa en UserAgents o modelos concretos, sino en grupos de UserAgents, de manera que sin llegar a tener un UserAgent ni su modelo registrado puede devolvernos las posibles caracteríscias comparándolo con un modelo semejante. Por ejemplo, es posible que las característias de un Nokia N73 que no tengamos registrado sean iguales o superiores a las de un Nokia N70, luego asumimos las características de este último. Esto es extremadamente útil a la hora de tratar con modelos nuevos ya que oficialmente Wurfl libera actualizaciones un par de veces al año, aunque puedes descargar las versiones diarias del cvs.

Originariamente, el procedimiento para obtener las características de un UserAgent se basaba en que, si no se encontraba un UserAgent idéntico al del cliente, se iba reduciendo la cadena del mismo un caracter hasta encontrar un UserAgent que conincidiese con esa cadena reducida. Obviamente muy eficiente no es el método, pero funcionaba hasta ahora. La introducción de navegadores avanzados en dispositivos de gama alta, derivados de Safari, Opera, etc. llevó a la aparición de UserAgents móviles derivados de Mozilla, como los navegadores web tradicionales. Nokia95 (y la mayoría de nuevos modelos de Nokia), iPhone, Blackberry… casi todos tienen cadenas del tipo:

Mozilla/5.0 (iPhone; U; CPU like Mac OS X; de-de) AppleWebKit/420.1
(KHTML, like Gecko) Version/3.0 Mobile/3B48b Safari/419.3

Mozilla/5.0 (SymbianOS/9.2; U; Series60/3.1 NokiaN95/11.0.026;
Profile MIDP-2.0 Configuration/CLDC-1.1) AppleWebKit/413
(KHTML, like Gecko) Safari/413

El método de reducción es poco eficiente con UserAgents de este tipo. Esto es, pues, lo que han cambiado. El nuevo sistema establece un análisis en dos pasos:

  1. Primero se averigua qué tipo de UserAgent estamos tratando (basado en Mozilla, Microsoft, etc.).
  2. Según el tipo del primer paso, se procesa el UserAgent con el manejador adecuado para encontrar la información requerida.

Por ejemplo, en el primer paso, si la cadena contiene la palara Blackberry sabemos que es un navegador de este tipo de dispositivos. Un algoritmo concreto para éstos nos devolverá el modelo.

Era una modificación imprescindible hoy en día. No por el iPhone como puedan pensar muchos, sino porque la mayoría de nuevos terminales de Nokia ya tienen derivados de Safari, navegadores que por cierto nos han dado algunos problemas desarrollando sites 100% correctos y estandard pero que no se visualizaban bien (a pesar de que en los navegadores de escritorio Safari sí que funcionaba bien).

Luca Passani, alma mater de Wurfl junto a Andrea Trasatti, lo explica mejor que yo aquí.

Artículos interesantes de los últimos días

Creando sencillas mashups y widgets con Google Ajax Feed Api

No cabe duda que uno de los mayores avances que se han producido en Internet ha sido el modelo de compartir contenidos generados. Desde la burbuja de 2001, donde se protegían los contenidos hasta el extremo, hemos llegado a un modelo donde prima lo contrario, compartirlos todo lo posible, cuantos más te lean mejor, da igual el medio. En esto han jugado un papel fundamental los sistemas de sindicación, concretamente los RSS. Hoy en día casi todo tiene feeds, da igual el tipo del contenido, no sólo artículos de texto, también fotos, vídeos, twitters y demás.

La teoría

La base de la mayoría de mashups o widgets es la combinación de distintos feeds y su posterior tratamiento y utilización. Un clásico cómo la combinación de fotos de Flickr con mapas de Google es sencillo gracias a los RSS con la información de la geolocalización del primero. La dificultad a la hora de gestionar los feed es la falta de una librería Javascript que permita leer y parsear feeds de cualquier tipo en el navegador sin ayuda de herramientas externas, por lo que hasta ahora lo normal era hacerlo del lado del servidor y devolver la lista de items del feed mediante JSON o simples arrays. Por suerte Google ha pensado en nosotros y nos ha dado la llave de la cerradura. La ventaja de hacerlo así es que todo el trabajo cae en el navegador cliente liberando a nuestro servidor de cargar los XML, parsearlos y hacer el tratamiento posterior. Además, para evitar problemas, tendríamos que cachearlos si no queremos que el rendimiento de nuestras máquinas caiga. Google ya lo hace todo por nosotros. Sí, también ha pensado en eso y no los recarga siempre que los llamas.

Con Google Ajax Feed Api es todo más sencillo puesto que es capaz de leer la mayoría de formatos de feeds y los devuelve en un objeto Javascript fácil de tratar. Para explicar su funcionamiento, nada mejor que un ejemplo práctico.

Hoy en Bricomanía…
Este es nuestro proyecto de hoy. Gracias a Marcos por su cuenta de vídeos en Youtube puesto que yo no tengo. Un pequeño widget con el que visualizar nuestra actividad en los servicios más populares de Internet. Lo puedes poner en el lateral de tu blog 😛 o añadir a tu MySpace.

La prática

Lo primero que haremos será registrar nuestro dominio para utilizar la API, con lo que obtendremos el API Key. Recuerda que esta clave va asociada al dominio y no podrás utilizarla en otro.

Lo siguiente será buscar las URLs de los feed’s a utilizar. En nuestro caso buscamos las de Twitter, Flickr, Youtube, Del.icio.us y LastFM:

Vosotros deberéis utilizar las vuestras, claro, tampoco es que yo tenga mucho interesante que ver o leer.

Veamos como hacerlo con uno de los feeds, los demás se harían del mismo modo. El resto del widget lo montáis como más os guste según lo que queráis hacer. En el caso de nuestro ejemplo definimos una capa (DIV) para cada feed y con los botones de las imágenes cambiamos el estilo display ocultando todas y mostrando la que se ha pulsado. Al final del articulo tenéis el código completo del ejemplo.

Lo primero que debemos hacer es cargar el Javascript de la API con el Api Key que obtuvimos. Ahora, como veis el código inferior, instanciamos la api y definimos una función de callback que se encargará de cargar nuestros feeds cuando se haya cargado la página. En esta función podemos añadir todo el código que queramos ejecutar para iniciar la aplicación. En nuestro caso vamos a cargar nuestro Twitter.

<script src="http://www.google.com/jsapi?key=ABQIAAAAVTgpBmlTiDeEs9UaP69iNRT4lrLtf4G1Npt4CNmX07qB4fHehhQysXL6wgFRWD8d-X_v8iu6Hs2orA" type="text/javascript"></script>
<script type="text/javascript">
function OnLoad() {
	loadTwitter();
}
google.load("feeds", "1");
google.setOnLoadCallback(OnLoad);
</script>

Ahora llega realmente el proceso. Veréis que pocas líneas son necesarias para parsear y mostrar tu Twitter.

function loadTwitter() {
    var feed = new google.feeds.Feed("http://twitter.com/statuses/user_timeline/"+usertwitter+".rss");
    feed.setNumEntries(50);
    feed.load(function(result) {
        var container = document.getElementById("twitter");
        if (!result.error) {
            var entries=result.feed.entries;
            var html="";
            for(var i=0; i<entries.length; i++){
                var entry = entries[i];
                var titulo=String((entry.title)).substring(String(entry.title).indexOf(":")+1);
                html+="<p><b>"+fecha(entries[i])+"</b><br/>"+string_create_urls(titulo)+"</p>";
            }
            container.innerHTML=html;
        }else{
            container.innerHTML="<p>Error cargando el feed</p>";
        }
    }
    )
}

El código casi se explica por sí mismo. Se carga el feed y se define la función que se ejecutará cuando esté cargado. Podemos incluso definir la cantidad máxima de items que se cargarán. Y ya está. Tenemos una colección de objetos con todos los datos, sólo queda iterar sobre ellos y mostrarlos como nos parezca oportuno. Nosotros nos hemos apoyado en un par de funciones auxiliares para mostrar la fecha y los links de nuestro Twitter.

Ahora me diréis: ¡traición! has utilizado innerHTML después de no recomendarlo en un artículo anterior. Bueno, de acuerdo, no tenía ganas de añadir más código y tampoco voy a necesitar acceder a lo añadido :-P, no es una buena excusa pero ahí queda eso. Sigo recomendando que utilicéis DOM para añadir los nuevos elementos. Creo que sería un buen tema para un artículo posterior puesto que el procedimiento es bastante engorroso, por eso innerHTML es tan popular.

Y eso es todo amigos. En el próximo programa veremos cómo posicionar las fotos de Flickr en Google Maps.

Aquí tenéis el ejemplo completo y el código.

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.