Renderizar html con PHP/FFI y wkhtmltox

Ando con un tema de conversión de contenidos para una web, es tema complicado del que no quisiera contar mucho. Implica un equipo de personas trabajando en remoto y publicación automática.

Como no puede ser de otra manera, yo estoy a cargo de la publicación automática, y nos ha surgido un pequeño problema: el sitio web para pruebas sólo es accesible en una de las oficinas, por lo que la herramienta que permite lanzar los contenidos contra el sitio de prueba para previsualizar los resultados no funciona en remoto.

Entre las muchas soluciones que podríamos aplicar la que nos gustaba era la de renderizar el resultado del sitio de prueba y mostrar la imagen resultante a los usuarios de la herramienta.

Existen muchas maneras de hacer un renderizado de html en PHP, bien con ImageMagick, mediante Imagick, bien convirtiendo el html en pdf… pero todas adolecen del mismo problema: no renderizan CSS o si lo hacen este tiene que ser incorporado, no enlazado.

Al final encontré wkhtmltopdf, una maravilla de la linea de comando. No sería la primera vez que ejecuto un comando desde PHP porque no he encontrado nada mejor.

Como decía, wkhtmltopdf es una maravilla porque:

  • Es código libre (LGPLv3)
  • Tienes binarios para Windows, Linux y MacOS
  • Realiza un renderizado muy bueno ya que utiliza el motor de renderizado Qt WebKit

Básicamente es un navegador en línea de comandos como Links, pero en lugar de ser en modo texto te renderiza la web en un pdf o imagen.

Con la primera prueba ya me convenció. Sin embargo con este proyecto he tenido la ocasión de probar e implementar una serie de técnicas e ideas a las que le tenia ganas desde hacía mucho tiempo y que ya iré contando. Y resulta que el wk me daba pie para probar otra técnica que tenía ganas de probar.

Se trata de la extensión FFI de PHP. Mola porque entre otras cosas la documentación está plagada de advertencias tipo:

Esta extensión es EXPERIMENTAL, allá tu con lo que hagas con ella. Esta extensión es peligrosa, permite el domino del mundo.

Documentación de PHP/FFI

Así que si es algo experimental y peligroso… ¿qué puede salir mal?.

¿Que es la extensión FFI de PHP? pues otra maravilla porque te permite acceder a librerías escritas en C desde PHP sin necesidad de crear una extensión.

En este caso concreto wk provee una librería llamada wkhtmltox para programar en C. Simplificando las cabeceras (los ficheros .h de C) y con una buena dosis de paciencia y de recordar cómo demonios funcionaba C con las cadenas de caracteres y los punteros he podido hacer lo que me proponía.

Vamos, acceder a la librería y hacer que te renderice una web en una imagen no es difícil, pero yo no quería crear un archivo, quiero crear una imagen que cargo en memoria y luego se la lanzo al cliente… o al menos esa es la idea por ahora.

Cuando al wk no le das un nombre de fichero para que guarde la imagen, almacena esta en un buffer interno. Mediante la función wkhtmltoimage_get_output puedes obtener dicho buffer interno.

Recordemos C

En C no todas las cadenas de caracteres son iguales. Tienes cadenas de caracteres, strings y cadenas binarias. Las dos primeras son muy similares, y para nuestro caso podemos considerarlas iguales… pero las cadenas binarias son otro asunto.

Una cadena binaria en C es un unsigned char, mientras que la cadena normal es char. Esto tiene su importancia porque PHP/FFI considera strings únicamente las cadenas de caracteres (char) y los string, las cadenas binarias no son strings. Pero me estoy saltando pasos.

Para recuperar el buffer interno lo más sensato, al menos en C, es hacer lo siguiente:

  1. Creas una variable de tipo puntero a una cadena binaria: unsigned char *
  2. A la función wkhtmltoimage_get_output le pasas un puntero a la variable anterior: un puntero a un puntero, unsigned char **
  3. La función lo que hace es asignarle a nuestro puntero la dirección del buffer, de esta manera hemos obtenido acceso al buffer sin mover ni un solo byte de este, y puede ser grande.

El punto dos es lo que se denomina pasar un parámetro por referencia, y en PHP/FFI hay que pasar dicho parámetro así: FFI::addr($buffer). Dicho de otra forma, hay que llamar a la función dándole la dirección a nuestro puntero.

El punto tres es una de las grandes ventajas y justificaciones de los punteros, hay mucho tras ellos y su comprensión nunca ha sido sencilla, pero si los comprendes has alcanzado la iluminación.

Bueno, tenemos nuestra cadena binaria en una variable que sólo entiende FFI, ¿cómo la convierto a una variable de PHP normal?

Esa pregunta, estimado lector, es lo que me costó unas horas entender. Tantos años programando el lenguajes de alto nivel hacen que se te olviden los rudimentos de un lenguaje de niveles inferiores, y recordemos: C está sólo un paso por encima de ensamblador, sólo un paso. PHP, Java, Node y muchos otros lenguajes comunes hoy en día están varios niveles por encima de C. Como si fuera la primera vez que me pasa.

Lo que entendí rápidamente es que para PHP una variable de tipo string es un puntero de tipo cadena char *, y una cadena binaria no es lo mismo, aunque unsigned char * se parezca. Así que el primer paso es hacer pasar una cadena binaria por una cadena normal: un casting de tipos que decimos los programadores. Esto en código es: FFI::cast('char *', $buffer).

Una vez que sabemos cómo podemos hacer pasar una cadena binaria por una cadena normal podemos recuperar el contenido de la misma en una variable normal de PHP: FFI::string(FFI::cast('char *', $buffer)).

Esta no es toda la historia ya que lo que conseguía no era la imagen completa, sólo me recuperaba los 8 primeros bytes de la imagen. Como es lógico estuve desconcertado un buen rato hasta que comprendí que la función FFI::string lo que hace es copiar un bloque de memoria de un lado a otro y el problema es que en las cadenas binarias no sabemos dónde termina el bloque a copiar si no le decimos exactamente cuanta información hay.

Me explico, las cadenas en C son muy sencillas: se ponen todos los caracteres uno detrás de otro, lo normal, y se sabe donde termina porque añade un 0 (\x00) al final. Era típico que las cadenas tuvieran un límite de 255 caracteres: el puntero tiene 8 bits, con ocho bits direccionamos 256 posiciones de memoria pero una la ocupa siempre el terminador de cadena (\x00), por lo que nos quedan 255 que podemos usar para lo que queramos.

El funcionamiento anterior es muy práctico y funciona muy bien… pero en una cadena binaria el 0 también es información válida, por lo que la idea del terminador de cadena ya no sirve y tenemos que almacenar la longitud de la cadena en otro sitio.

En QB lo que se hace es utilizar los dos primeros bytes de la cadena para guardar su longitud. C es más arcano que eso, es para programadores que saben lo que hacen y tu tienes que ser lo suficientemente inteligente para guardar la longitud de la cadena por tu cuenta.

En resumen, que era un error mío y le tenía que decir a la función la longitud de la cadena: FFI::string(FFI::cast('char *', $buffer), $len). Por ese motivo la función wkhtmltoimage_get_output nos devuelve la longitud de la cadena… si la propia función me lo estaba recordando, pero con la emoción yo ni caso.

Esta es otra de esas cosas que empaquetas en una clase y luego te olvidas de sus tripas, así que si os interesa el código en GitHub podéis descargarlo: https://github.com/fawno/PHP-wkhtmltox

Leave a Reply

Your email address will not be published. Required fields are marked *