Archivo de la etiqueta: PHP

Consumiendo webservices SOAP desde PHP

Recientemente nos han llegado un par de proyectos en los que debemos consumir webservices SOAP para obtener datos. La verdad es que hacía bastante tiempo que no los veía en mi trabajo habitual. Hace algunos años eran muy habituales para casi cualquier cosa que implicase comunicación con fuentes externas, sin embargo los últimos años habían caído un poco en desuso para cosas sencillas puesto que complicaban bastante un trabajo que con una simple petición HTTP y un XML básico se podría resolver. Precisamente ésta ha sido siempre una de las mayores críticas al protocolo SOAP, el elevado consumo de ancho de banda para una sencilla petición.

Hasta ahora siempre había utilizado nusoap para realizar llamadas SOAP desde PHP, sin embargo  me encontré con un problema al acceder a un servicio de un importante medio de comunicación internacional. La llamada con nusoap no devolvía resultados mientras que desde el sistema de pruebas html todo funcionaba correctamente.

Después de darle mil vueltas y no encontrar ningún error (otras peticiones al mismo servicio sí que funcionaban) me dí cuenta que PHP ya tiene un conjunto de funciones SOAP nativas, con lo que no necesitaría nusoap. La duda era saber si funcionaría bien, como así fue. Tras añadir la extensión adecuada surge una pequeña incompatibilidad. No puedes usar nusoap y las funcionas nativas en la misma instalación de PHP. Dicho de otro modo, si activas la extensión SOAP, nusoap dejará de funcionar y comenzará a lanzar mensajes de error ya que muchas de las funciones que utiliza tienen el mismo nombre que las nativas, que serían entonces nombres reservados. Si tienes alguna aplicación que utilice nusoap en la misma máquina tendrás que migrarla también para que utilice las funciones nativas.

El ejemplo de hoy será una sencilla llamada a un servicio que nos devuelve un listado de noticias.

El proceso es muy sencillo, necesitas la url del webservice, el método al que vas a llamar y los parámetros a pasarle y, como en cualquier servicio SOAP, te devolverá un XML.

$servicio="http://dominio.com/noticias?wsdl"; //url del servicio
$parametros=array(); //parametros de la llamada
$parametros['idioma']="es";
$parametros['usuario']="manolo";
$parametros['clave']="tuclave";$client = new SoapClient($servicio, $parametros);
$result = $client->getNoticias($parametros);//llamamos al métdo que nos interesa con los parámetros

Con estas sencillas instrucciones ya tenemos en $result el XML resultado de la llamada al servicio. Como trabajar con el XML es un poco engorroso, lo convertimos a un array asociativo de manera que nos sea más sencillo procesar los datos, para ello utilizamos la función obj2array que indico a continuación.

$result = obj2array($result);
$noticias=$result['resultado']['noticias'];
$n=count($noticias);

//procesamos el resultado como con cualquier otro array
for($i=0; $i<$n; $i++){
    $noticia=$noticias[$i];
    $id=$noticia['id'];
    //aquí iría el resto de tu código donde procesas los datos recibidos
}

function obj2array($obj) {
  $out = array();
  foreach ($obj as $key => $val) {
    switch(true) {
        case is_object($val):
         $out[$key] = obj2array($val);
         break;
      case is_array($val):
         $out[$key] = obj2array($val);
         break;
      default:
        $out[$key] = $val;
    }
  }
  return $out;
}

En la segunda línea nos quedamos con los elementos del array que nos interesa procesar. Si no sabes qué devuelve tu webservice puedes hacer un var_dump($result) y verás todo el resultado. En nuestro caso, como es una secuencia de noticias, nos quedamos con el elemento que tiene esas noticias.

Como os habréis dado cuenta, no me he preocupado del control de errores al llamar al webservice.  Eso os lo dejo como ejercicio a vosotros, que es agosto y no me apetece 😛 . En el manual de PHP está toda la información.

Y eso es todo amigos, hoy ha sido un ejemplo sencillo pero muy útil cuando necesitas utilizar SOAP.

Insertar en Sql-Server desde fuentes de datos con php en la línea de comandos

La aparentemente cosa más tonta del mundo me ha tenido todo el día liado. Y no es la primera vez. Anteriormente me había pasado al procesar RSS, hoy ha sido al consumir unos webservices (SOAP).

Nuestros sistemas trabajan con MsSql, Apache, PHP5 y Windows 2003 Server. De vez en cuando necesitamos crear tareas que lean desde determinadas fuentes de datos y los inserten en una base de datos. La última vez que me había ocurrido era leyendo un RSS. Si lo lanzaba a través de la línea de comandos con php.exe la codificación que se guardaba en la base de datos era errónea, todos los acentos, eñes y demás caracteres no ASCII se perdían. El caso es que al ejecutarlo a través de Apache todo funcionaba bien.

La otra vez, por falta de tiempo, lo dejé pasar y montamos la tarea a través de Apache con wget. Pero ahora no era la mejor solución, hay que procesar varios webservices y la tarea se puede prolongar durante mucho tiempo, con lo que tener un hilo de apache corriendo tanto tiempo no me parece lo más adecuado.

Después de mucho googlear he encontrado la solución. Resulta que al ejecutar la tarea desde la línea de comandos se produce una conversión automática de ANSI a OEM:

Any clients running Windows NT or Windows 95/98 are considered ANSI clients. Console-based applications, such as the isql utility, are considered OEM clients.

Por lo tanto nos cambia la codificación sin remedio. Para ello hay una solución, y es ejecutar nuestro script a través de un wrapper que lance el proceso sin ser bajo consola, digamos que engañando al sistema operativo. En tu instalación de php tendrás un php-win.exe que hace exactamente lo mismo que el cliente habitual pero sin lanzarlo en la línea de comandos, parecerá que no ha hecho nada pero si abres el administrador de tareas verás el proceso php-win.ese corriendo e insertando correctamente en la base de datos.

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.

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.

Redimensionando GIF’s desde PHP I

Como desarrollador de tecnologías para móviles, uno de los servicios que hacemos habitualmente es la venta de imágenes, fondos, wallpapers o como queráis llamarlo. Este servicio requiere el escalado de los archivos para adaptarlo al tamaño de pantalla del teléfono del cliente. Además de para la descarga de contenidos, el redimensionamiento de imágenes es necesario también para adaptar el look&feel de los sites al dispositivo del cliente. En un primer momento lo hacíamos con Imagemagick, pero, por alguna extaña razón, a medida que aumentaban las visitas se saturaba el Apache de las máquinas (Windows) puesto que, parece ser, el convert no finalizaba los procesos, y había que reiniciar los servidores web. La solución fue reemplazar el sistema basado en Imagemagick por otro basado en GD, utilizando las funciones nativas de PHP, de manera que todo quedaba integrado en la propia aplicación.

Os estaréis preguntando cual es el problema en el escalado de imágenes con GD. Ninguno, el resultado era perfecto hasta que nos encontramos con gif’s transparentes. Al hacer el escalado se perdía la transparencia sin remedio. Dimos muchas vueltas y probamos distintos métodos que fuimos leyendo por cientos y cientos de foros y blogs, puesto que en la documentación de PHP no quedaba nada claro cómo hacerlo hasta que dimos con la solución.

He decidido separar este artículo en dos partes. En el segundo trataremos el escalado de gif’s animados, un proceso bastante más complejo que los estáticos.

En un GIF, la transparencia viene definida por un color, un solo color, que se define como transparente, de manera que cada pixel que tenga exactamente ese color será transparente, de ahí que, tradicionalmente, se comience por una imagen de fondo blanco o negro y sobre ella se dibuje, guardando ese color de fondo inicial como el de transparencia. La transparencia es una característica de la imagen, no del color en sí. No tiene por qué ser blanco o negro, cualquier color puede ser el transparente, pero una vez definido, cualquier pixel que tenga ese color será transparente. Por otro lado, en un GIF se guarda la paleta de colores que utiliza, de manera que se optimiza el tamaño de la imagen manteniendo la información únicamente de los colores disponibles y referenciándolos por un índice dentro de esa paleta. Esto que puede parecer un dato sin importancia es fundamental a la hora de tener claro el proceso a seguir desde PHP.

Hay varias funciones de PHP que hacen referencia a la transparencia y a los colores. En unas se utilizan los colores como tal en formato RGB, en otras se utiliza el índice de color en la paleta y en otras se necesita la representación del color. Aquí está uno de los problemas, ¿cómo se debe utilizar el resultado de cada una de las funciones disponibles?.

Lo primero que debemos calcular es el porcentaje de escalado que vamos a aplicar. Esto dependerá de nuestra aplicación y del destino de la misma. Si queremos fijar un determinado ancho habrá que calcular el porcentaje sobre el que debemos escalar el alto y viceversa. No entraremos en detalles sobre este aspecto puesto que se sale fuera del objeto de este artículo.

Una vez tenemos claro el nuevo tamaño, debemos, antes de nada, saber cual es el color transparente de la imagen original. Para ello usamos la función imagecolortransparent, si hay transparencia nos devolverá el identificador de ese color. A partir del índice del color transparente, obtenemos la representación RGB en la imagen original con imagecolorsforindex y ya podemos localizar ese color en la nueva imagen con imagecolorallocatealpha. Ya tenemos todo lo necesario. Ahora simplemente rellenamos la nueva imagen con ese color, le decimos que ese color será el transparente y copiamos la imagen original en la nueva escalando según necesitamos.

A la hora de copiar la imagen y escalarla no utilizamos imagecopyresampled ya que, a diferencia de imagecopyresized, hace una interpolazión para suavizar el resultado final. Esto, que en general es correcto, en el gif transparente provoca la deformación de pixeles propios de la interpolación, haciendo que se pierda parte de la trasparencia ya que estos pixeles no son del color exacto.

Este sería el código final.

$srcImage=imagecreatefromgif("demo.gif");
$new_w=160;
$new_h=114;
$dst_img=imagecreatetruecolor($new_w,$new_h);
$transcolor=imagecolortransparent($srcImage);
if($transcolor!=-1){
 $trnprt_color = imagecolorsforindex($srcImage, $transcolor);
 $trnprt_indx = imagecolorallocatealpha($dst_img, $trnprt_color['red'], $trnprt_color['green'], $trnprt_color['blue'], $trnprt_color['alpha']);
 imagefill($dst_img, 0, 0, $trnprt_indx);
 imagecolortransparent($dst_img, $trnprt_indx);
}
imagecopyresized($dst_img,$srcImage,0,0,0,0,$new_w,$new_h,ImageSX($srcImage),ImageSY($srcImage));

Para muestra, un color. Con esta imagen de fondo transparente (sí, vale que no es un Picasso, pero para el ejemplo sirve :P):

Demo Transparente

Conseguimos esto:

Demo Transparente Redimensionada