Redimensión de imágenes proporcionalmente en PHP

Una necesidad que surge en todas las webs cuyos contenidos son introducidos por un usuario (o varios) a través de un gestor de contenidos, es adaptar las imágenes que estos suben al diseño de la misma para que no se deforme. Además, las proporciones de una imagen en una sección pueden ser diferentes a cómo se muestra en otra; un caso típico es usar una proporción para un listado y otra para la ficha de cada elemento, por ejemplo las noticias, los productos, etc.

Para resolver este problema sirve el conjunto de clases PHP que presentó en este artículo. Este software, a partir de una imagen original, creará una copia por cada tamaño que necesitemos. El proyecto completo puede verse en Github y el objetivo del presente artículo es explicar cómo funciona. En primer lugar, instanciamos la clase:

$objResize = Signia_ImageResize_Factory::getInstanceOf($srcFile, $destFile, $newSize);

Donde:

  • $srcFile es el path hacia la imagen que ha subido el usuario.
  • $desFile es el path de la imagen de destino.
  • $newSize es el tamaño deseado.

A partir de un array con los tamaños deseados podemos crear un bucle por cada uno de ellos ($newSize). Los índices widthMaxheigthMax hacen referencia al tamaño máximo permitido, mientras que width y height pueden entenderse como widthMin y heightMin. Veamos un ejemplo:

$imageType = [
  'slide' => ['width' => 1000, 'widthMax' => 1000, 'height' => 400, 'heightMax' => 400, 'background' => '000000'],
  'r2_34' => ['width' => 468, 'widthMax' => 2340, 'height' => 200, 'heightMax' => 1000],
  'r2_7'  => ['width' => 1080, 'widthMax' => 2700, 'height' => 400, 'heightMax' => 1000],
  'r1_6'  => ['width' => 459, 'widthMax' => 1000, 'height' => 287, 'heightMax' => 625],
  'r1'    => ['width' => 266, 'widthMax' => 266, 'height' => 177, 'heightMax' => 177]
];

En Factory.php podemos ver la clase Signia_ImageResize_Factory:

class Signia_ImageResize_Factory
{
	static public function getInstanceOf($srcImageName, $destImageName, $newSize)
	{
		$aux       = explode(".", $destImageName);
		$extension = end($aux);
		if (preg_match("/jpg|JPG|jpeg|JPEG/", $extension)) {
			$extension = "jpeg";
		}
		$imageResizer = "Signia_ImageResize_" . ucfirst($extension);

		return new $imageResizer($srcImageName, $destImageName, $newSize);
	}
}

Su cometido es invocar la clase que corresponda según la extensión del fichero: Signia_ImageResize_Jpeg, Signia_ImageResize_Gif, etc. Todas ellas tienen el mismo objetivo: crear un recurso imagen a partir del fichero para, más adelante, poder manipular el tamaño de la imagen. Observemos el código de una de ellas, Signia_ImageResize_Jpeg:

class Signia_ImageResize_Jpeg extends Signia_ImageResize_Abstract
{
	function getResizedImage()
	{
		if (!file_exists($this->srcImageName)) {
                    return $this->error_message;
                }
      
                $this->srcImage = @imagecreatefromjpeg($this->srcImageName);
                if ($this->initCheck() && $this->sizeControl()) {
                      $this->destImage = imagecreatetruecolor($this->destWidth, $this->destHeight);
                      if ($this->background) {
                            $this->resizeImageWithBackground();
                      }else{
                           $this->resizeImage();
                      }
                      imagejpeg($this->destImage, $this->destImageName, 50);
         
                      return true;
                }		
	}
}

El tercer parámetro de imagejpeg() es un número entre 0 y 100 que especifica la calidad de la imagen. Obviamente, una mayor calidad implica también un fichero más grande. Google da mucha importancia a la velocidad de carga del sitio a la hora de situarlo entre los resultados de búsqueda, y con 50 se consigue un fichero de peso reducido sin sacrificar demasiada calidad. Esto es especialmente importante si tenemos en cuenta que muchos usuarios descuidan este tema, pudiendo incluso llegar a subir imágenes de más de un 1 MB (siendo esta práctica compatible con la quejas acerca de la velocidad de carga de su sitio).

Si el lector observa las clases Signia_ImageResize_Png y Signia_ImageResize_Gif, comprobará que hay unos extras para que, a pesar de la manipulación de la imagen, no se pierda la transparencia, efecto que sólo puede existir en estos dos formatos de imagen.

class Signia_ImageResize_Gif extends Signia_ImageResize_Abstract
{
   function getResizedImage()
   {
      if (!file_exists($this->srcImageName)) {
         return $this->error_message;
      }
      $this->srcImage = imagecreatefromgif($this->srcImageName);
      if ($this->initCheck() && $this->sizeControl()) {
         $transparency = imagecolortransparent($this->srcImage);
         if ($transparency != -1) {
            $this->destImage    = imagecreatetruecolor($this->aResult[0], $this->aResult[1]);
            $colorTransparent   = imagecolorsforindex($this->srcImage, $transparency);
            $idColorTransparent = imagecolorallocatealpha($this->destImage, $colorTransparent['red'], $colorTransparent['green'], $colorTransparent['blue'], $colorTransparent['alpha']);
            imagefill($this->destImage, 0, 0, $idColorTransparent);
            imagecolortransparent($this->destImage, $idColorTransparent);
         } else {
            $this->destImage = imagecreatetruecolor($this->aResult[0], $this->aResult[1]);
         }
         if ($this->background) {
            $this->resizeImageWithBackground();
         }else{
            $this->resizeImage(true);
         }
         imagegif($this->destImage, $this->destImageName);
         return true;
      }
   }
}

Es desde estas clases donde se llama a la parte más compleja del código, la que realmente redimensiona la imagen: Abstract.php. Básicamente, la imagen que sube el usuario puede diferir en cuanto a las proporciones deseadas en si el ratio de la imagen original es menor o mayor. De no diferenciar el proceso para actuar de forma disitinta en cada caso, algunas imágenes se deformarán, tal y como pasa en los ejemplos que he visto en Stack Overflow sobre el tema. Para entender cómo funciona, bastará con explicar uno sólo de los casos: cuando el destino es mayor. Primero, veamos el contenido de la clase:

abstract class Signia_ImageResize_Abstract
{

   protected $srcImage;
   protected $srcImageName;
   protected $srcWidth;
   protected $srcHeight;
   protected $srcRatio;
   protected $destImage;
   protected $destImageName;
   protected $destWidth;
   protected $destHeight;
   protected $destRatio;
   protected $widthMin;
   protected $heightMin;
   protected $widthMax;
   protected $heightMax;
   protected $background;
   protected $bincolor;
   protected $error;
   public $error_message;
   protected $aMessages = array(
       'src_not_found'      => "El fichero de origen no se encuentra",
       'not_enought_params' => "No se han definido los tamaños de salida",
       'size_values'        => "La imagen es demasiado pequeña para generar las copias",
       'ratio'              => "No se ha podido calcular el ratio destino de la copia"
   );
   protected $aResult   = [];

   public function __construct($srcImageName, $destImageName, $newSize)
   {
      $this->srcImageName   = $srcImageName;
      $this->destImageName  = $destImageName;
      $this->widthMin       = (!isset($newSize['width'])) ? 1 : $newSize['width'];
      $this->heightMin      = (!isset($newSize['height'])) ? 1 : $newSize['height'];
      $this->widthMax       = (!isset($newSize['widthMax']) || $newSize['widthMax'] > 8192) ? 8192 : $newSize['widthMax'];
      $this->heightMax      = (!isset($newSize['heightMax']) || $newSize['heightMax'] > 6144) ? 6144 : $newSize['heightMax'];
      $this->background     = (!isset($newSize['background'])) ? null : $newSize['background'];
      $this->sizeControlled = (!isset($newSize['sizeControlled'])) ? true : $newSize['sizeControlled'];
      if ($this->background) {
         $this->setBincolor();
      }
   }

   public function __destruct()
   {
      if (isset($this->destImage) && is_resource($this->destImage)) {
         imagedestroy($this->destImage);
      }
      if (isset($this->srcImage) && is_resource($this->srcImage)) {
         imagedestroy($this->srcImage);
      }
   }

   /**
    * Si ponemos minWidth y width al mismo valor, e igualamos también minHeight y Height,
    * y la imagen que se sube no es del mismo ratio, saldrán franjas por los lados o arriba y abajo.
    * El background indica el color de estas franjas.
    */
   protected function resizeImageWithBackground()
   {
      if ($this->srcRatio > $this->destRatio) {
         $aux1 = floor($this->srcWidth / $this->destWidth * ($this->destHeight - $this->destWidth / $this->srcWidth * $this->srcHeight));
         $aux2 = floor(($this->destHeight - $this->destWidth / $this->srcWidth * $this->srcHeight) / 2);
         imagecopyresampled($this->destImage, $this->srcImage, 0, 0, 0, -$aux1 / 2, $this->destWidth, $this->destHeight, $this->srcWidth, $this->srcHeight + $aux1);
         $aux3 = imagecolorallocate($this->destImage, $this->bincolor[0], $this->bincolor[1], $this->bincolor[2]);
         imagefilledrectangle($this->destImage, 0, 0, $this->destWidth, $aux2 + 1, $aux3);
         imagefilledrectangle($this->destImage, 0, $this->destHeight - $aux2 - 1, $this->destWidth, $this->destHeight, $aux3);
      } elseif ($this->srcRatio < $this->destRatio) {
         $aux1 = floor($this->srcHeight / $this->destHeight * ($this->destWidth - $this->destHeight / $this->srcHeight * $this->srcWidth));
         $aux2 = floor(($this->destWidth - $this->destHeight / $this->srcHeight * $this->srcWidth) / 2);
         imagecopyresampled($this->destImage, $this->srcImage, 0, 0, -$aux1 / 2, 0, $this->destWidth, $this->destHeight, $this->srcWidth + $aux1, $this->srcHeight);
         $aux3 = imagecolorallocate($this->destImage, $this->bincolor[0], $this->bincolor[1], $this->bincolor[2]);
         imagefilledrectangle($this->destImage, 0, 0, $aux2 + 1, $this->destHeight, $aux3);
         imagefilledrectangle($this->destImage, $this->destWidth - $aux2 - 1, 0, $this->destWidth, $this->destHeight, $aux3);
      }
   }

   /**
    * @param boolean $transparent Para preservar transparencia (imágenes gif y png)
    */
   protected function resizeImage($transparent = false)
   {
      if ($this->srcRatio > $this->destRatio) { // En el original hay más width por cada píxel de height.
         $canvas = imagecreatetruecolor($this->destHeight * $this->srcRatio, $this->destHeight);
         if ($transparent) {
            imagealphablending($canvas, false);
            imagesavealpha($canvas, true);
         }
         $destWidth = $this->destHeight * $this->srcRatio;
         // $canvas contendrá la imagen original con el height del destino y el width recortado proporcionalmente según srcRatio:
         imagecopyresampled($canvas, $this->srcImage, 0, 0, 0, 0, $destWidth, $this->destHeight, $this->srcWidth, $this->srcHeight);
         $auxWidth  = $this->destHeight * $this->srcRatio; // $canvas no es recortada desde x = 0, es decir, no se recorta sólo por la derecha sino también por la izquierda
         imagecopyresampled($this->destImage, $canvas, 0, 0, ($auxWidth - $this->destWidth) / 2, 0, $this->destWidth, $this->destHeight, $this->destWidth, $this->destHeight);
      } elseif ($this->srcRatio < $this->destRatio) { // Menos width por cada height en el original.
         $inverseSrcRatio = $this->srcHeight / $this->srcWidth;
         $canvas          = imagecreatetruecolor($this->destWidth, $this->destWidth * $inverseSrcRatio);
         if ($transparent) {
            imagealphablending($canvas, false);
            imagesavealpha($canvas, true);
         }
         imagecopyresampled($canvas, $this->srcImage, 0, 0, 0, 0, $this->destWidth, $this->destWidth * $inverseSrcRatio, $this->srcWidth, $this->srcHeight);
         $auxHeight = $this->destWidth * $inverseSrcRatio;
         imagecopyresampled($this->destImage, $canvas, 0, 0, 0, ($auxHeight - $this->destHeight) / 2, $this->destWidth, $this->destHeight, $this->destWidth, $this->destHeight);
      } else {
         imagecopyresampled($this->destImage, $this->srcImage, 0, 0, 0, 0, $this->destWidth, $this->destHeight, $this->srcWidth, $this->srcHeight);
      }
   }

   protected function initCheck()
   {
      if (!file_exists($this->srcImageName)) {
         $this->error         = true;
         $this->error_message = $this->aMessages['src_not_found'];
         return false;
      }
      if (!$this->widthMax && !$this->heightMax && !$this->widthMin && !$this->heightMin) {
         $this->error         = true;
         $this->error_message = $this->aMessages['not_enought_params'];
         return false;
      }
      if (!is_resource($this->srcImage)) {
         return false;
      }
      $this->srcWidth  = imagesx($this->srcImage);
      $this->srcHeight = imagesy($this->srcImage);
      $this->error     = false;
      return true;
   }

   protected function sizeControl()
   {
      if (!isset($this->background) && $this->sizeControlled) {
         if ($this->srcWidth < $this->widthMin || $this->srcHeight < $this->heightMin) {
            $this->error         = true;
            $this->error_message = $this->aMessages['size_values'];
            return false;
         }
      }
      // Resolución de incongruencias del tamaño solicitado en $imageType
      if ($this->widthMin > $this->widthMax) {
         $this->widthMin = $this->widthMax;
      }
      if ($this->heightMin > $this->heightMax) {
         $this->heightMin = $this->heightMax;
      }
      // Fin resolución.
      $widthAux  = $this->srcWidth;
      $heightAux = $this->srcHeight;
      //Primero recorta por el ancho:
      if ($this->srcWidth > $this->widthMax) {
         $widthAux  = $this->widthMax;
         $heightAux *= $this->widthMax / $this->srcWidth;
      }
      /* Después por el alto. Si la altura inicial es inferior a la mínima después 
       * de haber sido recortada heightAux = heightMin y widthAux = ...
       */
      if ($heightAux < $this->heightMin) {
         $heightAux = $this->heightMin;
         // Si no, si el alto es superior al máximo, se recorta, volviendo a adaptar el width.
      } elseif ($heightAux > $this->heightMax) {
         if (($this->heightMax / $heightAux) * $widthAux < $this->widthMin) { // Siempre >= 1
            $widthAux = $this->widthMin;
         } else {
            $widthAux *= $this->heightMax / $heightAux;
         }
         $heightAux = $this->heightMax;
      }
      $this->srcRatio   = $this->srcWidth / $this->srcHeight;
      $this->destRatio  = $widthAux / $heightAux;
      $this->destWidth  = (int) $widthAux;
      $this->destHeight = (int) $heightAux;
      return $this;
   }

   private function setBincolor()
   {
      $hexcolor       = str_split($this->background, 2);
      $bincolor[0]    = hexdec('0x{' . $hexcolor[0] . '}');
      $bincolor[1]    = hexdec('0x{' . $hexcolor[1] . '}');
      $bincolor[2]    = hexdec('0x{' . $hexcolor[2] . '}');
      $this->bincolor = $bincolor;
      return $this;
   }
}

Este es un esquema del proceso:

El rectángulo más grande representa la imagen original, mientras el rectángulo más pequeño que contiene representa el tamaño deseado, cada uno tiene representado su ratio o razón en forma de diagonal. En el método resizeImage() se hace, en primer lugar, una redimensión de la imagen en donde la altura pasa a ser la de la imagen de destino y el ancho se adapta proporcionalmente, la imagen resultante se guarda en la variable de tipo recurso $canvas. A continuación, se procede a recortar, pero no desde la esquina inferior izquierda, sino desde un punto x al que se le suma $this->destWidth, de este modo, se recorta la misma cantidad por el lado izquierdo por el derecho. La razón de esto es que las imágenes tienden a tener el objeto principal en el centro.

El esquema también incluye un ejemplo numérico en el que la imagen original tiene un ratio de 1’3 periódico, es decir, unos 13 píxeles de ancho por cada 10 de alto (no tiene mucho sentido hablar de 1’3 píxeles pues es una unididad indivisible), mientras que el ratio deseado es de 0’665.

Cuando el destino tiene un ratio mayor, podemos ver en el siguiente esquema que la idea es la misma, sólo que en el redimensionamiento inicial lo que se adapta proporcionalmente es el alto a partir de un ancho que se fija:

Esquema reducción imagenEl método initCheck() comprueba, en primer lugar, que exista la imagen original para, a continuación comprobar que el array $imageType tiene todos los parámetros esperados. Finalmente, obtiene el ancho y el alto de la imagen original.

El objetivo del método sizeControl() consiste en calcular el ancho y el alto de la imagen de destino, así como el ratio de esta y la original. Previamente, resuelve incongruencias en los parámetros, como, por ejemplo, que el ancho mínimo tolerado (widthMin) sea menor que el máximo (widthMax).

Veamos a continuación un ejemplo: los tamaños r1, r1_6  y slide que aparecen al principio del presente artículo a partir esta imagen:

Júpiter

Hacer click para obtener la imagen original, aunque WordPress redujo algo su peso.

Tamaño r1_6

Tamaño r1_6. Si hacemos click sobre la imagen podremos apreciar que su tamaño coincide con el especificado y que el peso del fichero se ha reducido mucho, más allá de lo que sería proporcional a la reducción de dimensiones, sin afectar, a simple vista, a la calidad de la imagen.

Tamaño r1

Tamaño r1

El comportamiento es diferente cuando, por un lado existe el parámetro background y, por el otro, height y maxHeight valen lo mismo, así como width y maxWidth, pero la imagen subida no es del mismo ratio. En este caso, aparecerán unas franjas horizontales o verticales en el color de este parámetro, lo cual es útil en circuntancias en que queremos que la imagen que se visualiza en la web sea de un tamaño exacto, como podría ser, por ejemplo, en el caso de slides.

Júpiter slide

Generalmente, es conveniente que el color de las franjas coincida con el color de fondo de la web, aunque en este caso se ha escogido el negro.

Funciones de orden superior

Tanto en matemáticas como en informática, las funciones de orden superior son aquellas que cumplen, al menos, una de estas condiciones:

  1. Esperan como argumento/s una o más funciones.
  2. Devuelven una función como resultado.

Ejemplos en matemáticas son la derivada y la antiderivada o función primitiva.

operador diferencial

El operador diferencial es una función de orden superior

Antiderivada

La antiderivada de una función f es una función F tal que F’ = f

En informática son la esencia de los lenguajes funcionales, pero también aparecen en lenguajes de otros paradigmas. Este es un ejemplo en el lenguaje Scheme en el que la función (f x) recibe un argumento y devuelve una función:

(define (f x)
  (lambda (y) (+ x y)))
(display ((f 3) 7))

Puede ejecutarse aquí para ver el resultado.

Cuando nació Javascript, a algunos programadores les pareció un lenguaje orientado a objetos fallido1, sobretodo porque, por razones comerciales, se le puso un nombre que lo asocia con Java. Desconozco si su creador estuvo muy de acuerdo con ese nombre pues, tal y como se diseño este lenguaje, da bastante juego a la programación funcional. En el siguiente ejemplo, el método filter() es una función de orden superior, pues espera recibir una función como parámetro:

function isPrime(x){
  if (x === 2) {
     return true;
  }
  let test = x%2 !== 0;
  let i = 3;
  stop = Math.floor(Math.sqrt(x)); // Raíz entera de x
  while (test && i <= stop) {
	  test = x%i !== 0;
	  i = i + 2;
  }
  return test;
}

const numbers = [47, 139, 137, 213, 2, 3, 45, 1515];
const primeNumbers = numbers.filter(isPrime);
console.log(primeNumbers);

Lo que este programa hace es filtrar la formación de números naturales “numbers“, dejando sólo los que sean primos en “primeNumbers“. Cada elemento de “numbers” será evaluado por la función “isPrime” mediante la criba de Eratóstenes. El lector puede ejecutarlo accediendo a la consola del navegador pulsando F12 y modificar el valor de “numbers” con los números (o el número) que quiera saber si son primos o no.

Este tipo de funciones están en prácticamente todos los lenguajes modernos, incluso en los que no se tuvo en cuenta el paradigma funcional en el momento de su creación. Es el caso de PHP, donde podemos encontrar una gran cantidad de funciones que esperan otra función, como es el caso de, por ejemplo, preg_replace_callback()2:

$capitalice = function($coincidencia) {
    return strtoupper($coincidencia[1]);
};

echo preg_replace_callback('~-([a-z])~', $capitalice, 'hola-mundo');

Además de usar las implementadas en funciones y métodos propios del lenguaje, también podemos crear las nuestras, de forma parecida a un lenguaje completamente funcional. En Javascript, la Wikipedia nos ofrece el siguiente ejemplo:

const twice = (f, v) => f(f(v));
const add3 = v => v + 3;

console.log(twice(add3, 7));

Lo mismo es posible en PHP:

$twice = function($f, $v) {
    return $f($f($v));
};

$f = function($v) {
    return $v + 3;
};

echo($twice($f, 7));

La programación funcional pretende tratar la programación como la evaluación de funciones matemáticas, paradigma muy diferente a la programación imperativa, basada en estados y en instrucciones que lo cambian. Tal vez las características funcionales que tienen algunos lenguajes puedan ayudarnos a introducirnos en un paradigma, el funcional, que nos exige una forma muy distinta de enfocar los problemas.


 

1 Todavía hoy en día, y a pesar de los cambios que ha sufrido en los últimos ECMA, sigue despertando las críticas de los programadores que, debido a su nombre, esperan que se comporte como un lenguaje completamente orientado a objetos, como Java, y se dan de bruces contra la realidad.

2 El parámetro que recibe la función contenida en $capitalize son las coincidencias que encuentre la expresión regular.

Decimales del número e por Taylor

Tanto Jhon von Neumann como Steve Wozniack hicieron el ejercicio de encontrar miles de decimales del número e, en el ENIAC (uno de los primeros ordenadores de propósito general) y en un Apple II, respectivamente. Si personas de tan elevado tamaño intelectual consideraron oportuno hacerlo, ¿quién soy yo para contrariarlos? Así que, salvando las distancias, me he decidido a hacerlo.

El método empleado han sido las series de Taylor. Para el mismo, se requiere calcular el factorial, la siguiente función recursiva lo calcula para el número recibido como parámetro:

function factorial(a) {
  if (a === 0) {
      return 1;
  }else{
      return a * factorial(a - 1);
  }
}

El siguiente código Javascript calcula, a partir del polinomio de Taylor1 de grado 20 en torno al punto a=0, es decir, e1, unos cuantos decimales:

function factorial(a) {
  if (a === 0) {
      return 1;
  }else{
      return a * factorial(a - 1);
  }
}

var e = 1;

for (i = 1; i <= 19; i++) {
   e = e + 1/factorial(i);
}
console.log(e);

El lector puede copiar el código y ejecutarlo en el mismo navegador con el que está leyendo este artículo, presionando F12 y accediendo a la consola:

Decimales de e en Javascript por Taylor

Decimales de e en Javascript por Taylor. Ejecutado en Firefox.

Por culpa del uso de la coma flotante que hace Javascript (ya incluido en el microprocesador), no podemos sacar más decimales, pues el resultado será redondeado al que vemos en la imagen. Para salvar este inconveniente, en PHP existe la librería BC Math, que nos permite trabajar con números de – casi – cualquier tamaño y precisión. Para que después digan que es un lenguaje de juguete… Éste es el código:

<?php

function factorial($a) {
  if ($a === 0) {
      return 1;
  }else{
      return bcmul($a, factorial($a - 1), 3000);
  }
}

$e = '2';

for ($i = 2; $i <= 1000; $i++) {
   $e = bcadd($e, bcdiv('1', factorial($i), 3000), 3000);
}
echo ($e);

Con el que se obtienen 2572 dígitos correctos:

2.71828182845904523536028747135266249775724709369995957496696762772407663035354759457138217852516642742746639193200305992181741359662904357290033429526059563073813232862794349076323382988075319525101901157383418793070215408914993488416750924476146066808226480016847741185374234544243710753907774499206955170276183860626133138458300075204493382656029760673711320070932870912744374704723069697720931014169283681902551510865746377211125238978442505695369677078544996996794686445490598793163688923009879312773617821542499922957635148220826989519366803318252886939849646510582093923982948879332036250944311730123819706841614039701983767932068328237646480429531180232878250981945581530175671736133206981125099618188159304169035159888851934580727386673858942287922849989208680582574927961048419844436346324496848756023362482704197862320900216099023530436994184914631409343173814364054625315209618369088870701676839642437814059271456354906130310720851038375051011574770417189861068739696552126715468895703503540212340784981933432106817012100562788023519303322474501585390473041995777709350366041699732972508868769664035557071622684471625607988265178713419512466520103059212366771943252786753985589448969709640975459185695638023637016211204774272283648961342251644507818244235294863637214174023889344124796357437026375529444833799801612549227850925778256209262264832627793338656648162772516401910590049164499828931505660472580277863186415519565324425869829469593080191529872117255634754639644791014590409058629849679128740687050489585867174798546677575732056812884592054133405392200011378630094556068816674001698420558040336379537645203040243225661352783695117788386387443966253224985065499588623428189970773327617178392803494650143455889707194258639877275471096295374152111513683506275260232648472870392076431005958411661205452970302364725492966693811513732275364509888903136020572481765851180630364428123149655070475102544650117272115551948668508003685322818315219600373562527944951582841882947876108526398139559900673764829224437528718462457803619298197139914756448826260390338144182326251509748279877799643730899703888677822713836057729788241256119071766394650706330452795466185509666618566470971134447401607046262156807174818778443714369882185596709591025968620023537185887485696522000503117343920732113908032936344797273559552773490717837934216370120500545132638354400018632399149070547977805669785335804896690629511943247309958765523681285904138324116072260299833053537087613893963917795745401613722361878936526053815584158718692553860616477983402543512843961294603529133259

Aquí está el link que ejecuta este código:

https://www.victoriglesias.net/e.php

Al seguir el link, el lector con interés en el tema descubrirá que proporciona más decimales, pero, como dije, sólo los primeros 2572 son correctos. Supongamos que sabemos que e está por debajo de 3, pero no podemos concretar más, el error, por el residuo del teorema de Taylor, sería 3/(i + 1)!, es decir:

+-0.00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000744805637342692...

Por cierto, no por acotar mejor e, por ejemplo 2’8, dejaríamos de obtener menos posiciones a 0 hasta obtener los primeros decimales del error. 2’8/(i + 1)! es:

+-0.00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000695151928186513...

Por lo tanto, aunque acotemos con más precisión e, la fórmula del residuo nos sigue asegurando que hemos calculado correctamente la misma cantidad de decimales.

Si parecen insuficientes y se desea mayor precisión, bastará con incrementar el tope de la variable i del bucle a más de 1000. Eso sí, si nos tomamos este cálculo en serio, sería aconsejable optimizar previamente la función recursiva.

En definitiva, aunque me quedé muy lejos del récord mundial ;-), fue entretenido hacerlo y espero que al lector le parezca interesante también o, por lo menos, curioso.


1 Se llama serie de Maclaurin el caso concreto de Taylor a=0

Modificar imágenes desde la línea de comandos con ImageMagick

ImageMagick es un completísimo paquete de programas multiplataforma para el tratamiento de imágenes. A diferencia de programas como Photoshop o Gimp, los programas que lo componen se ejecutan desde la línea de comandos, aunque también dispone de un sencillo entorno para X Window en Linux.

Entorno gráfico de ImageMagick en Linux

Tiene numerosas herramientas que, entre otras cosas, permiten:

  • Convertir entre una gran cantidad de formatos de imágenes.
  • Manipular la paleta de colores de una imagen.
  • Añadir tramados.
  • Crear un gif animado a partir de varias imágenes.
  • Superponer una imagen encima de otra.
  • Añadir texto a una imagen.

En la documentación se puede ver un listado detallado.

En la web es frecuente que el gestor de contenidos cree automáticamente réplicas de diferentes tamaños a partir de una imagen subida por el usuario para su correcta visualización en diferentes páginas. Por lo tanto, sin duda lo más usado para la web es su capacidad de reescalar imágenes, para lo que usa el algoritmo de “liquid rescaling“. Ademas la modificación de tamaño es muy flexible, permitiéndonos lograr la modificación que estemos buscando.

Tal vez algún lector se esté preguntando que habiendo programas con entorno gráfico como los citados más arriba, para qué trabajar en el modo texto de la shell de Linux/Unix. Veamos entonces un caso en que sería conveniente. Resulta fácil que una web acabe teniendo miles de imágenes distribuidas en cientos o miles de directorios, pues, por cada imagen que el administrador del contenido sube se van a crear varias copias con diferentes tamaños. Fácilmente el total de imágenes de un catálogo de productos, por ejemplo, se vuelve inmenso. Imaginemos que un cambio de diseño o la necesidad de hacer la web responsive hace necesario modificar el tamaño de una de las copias que se estaba guardando de cada imagen. En vez de modificarlas una a una con nuestro programa de edición de imágenes favorito, se puede crear un shell script como este:

ls *jpg | while read i
do
f=`basename $i .jpg`
convert $i -resize 200x200 smaller-$f.jpg
done

Lo que hace es buscar todas las imágenes jpg del directorio y gracias al comando convert de ImageMagick crea una copia más pequeña reescalada a 200×200 píxeles cuyo nombre será la cadena “smaller-” concatenada al nombre del fichero original. Fácilmente se podría modificar el script para que recorriera recursivamente todos los subdirectorios.

Todo lo que nos ofrece ImageMagick se puede utilizar desde diferentes lenguajes de programación y no solo porque desde éstos se pueda invocar comandos en la shell* y por lo tanto a ImageMagick, si no que el paquete ha sido portado. Está disponible en Java, .NET, Ruby, Pearl, Python, Php, etc. Aquí hay una lista detallada de todos.

En el caso concreto de PHP existe la extensión nativa IMagick. Sin duda es GD la extensión más extendida, valga la redundancia, en PHP. A pesar de su mayor propagación, y como es tan frecuente en el mundo de la informática, IMagick presenta algunas ventajas sobre GD que la convierten en una mejor extensión para trabajar con imágenes. Aquí un artículo con una comparación entre ambas.


* Como hace Drupal, el extendido CMS en PHP.


Aunque la palabra comando es un calco de inglés y en español es más correcto usar orden o instrucción, uso el término que usamos habitualmente en nuestro trabajo.


 

Clase que permite transacciones anidadas con PDO

PDO, acrónimo de PHP Data Objects es un interfaz para acceder a bases de datos de PHP. A diferencia de las extensiones mysql y mysqli, que son exclusivas para MySQL, PDO puede trabajar con diferentes sistemas gestores de bases de datos, siempre y cuando haya driver para ella.

Las transacciones sirven para garantizar la integridad referencial de los datos. Una transacción está formada por varias órdenes SQL y bien se ejecutan todas las consultas en bloque o si alguna falla se vuelve al estado inicial antes de empezar la transacción, sin ejecutarse ninguna de ellas. Las transacciones deben cumplir con las propiedades ACID: Atomicidad, Consistencia, Isolation (aislamiento) y Durabilidad. (Nada que ver con el subgénero de música electrónica de finales de los 80 🙂 )

Las transacciones se anidan cuando existiendo una transacción en curso, se inicia otra. Esto por ejemplo sucede cuando desde un método donde se ha iniciado una transacción, para reciclar código (una de las virtudes de la programación orientada a objetos) se llama a otro método que inicia otra transacción.

Transacciones anidadas

Todo fue bien hasta el final y se ejecutan en bloque todas las transacciones (COMMIT)

Transacciones anidadas

¡Ups, algo fallo cuando ya casi finalizaba! ¡Déjalo todo como estaba! (ROLLBACK)

Desgraciadamente, desarrollando en LAMP, nada más iniciarse la segunda transacción se producirá un error fatal. MySQL no soporta las transacciones anidadas. En su documentación afirma que después de ejecutarse un BEGIN TRANSACTION ciertas órdenes producirán un COMMIT, entre ellas BEGIN TRANSACTION. Tampoco las soporta PostgreSQL. Una solución parcial que ambos sistemas incorporan son los SAVE POINTS.

A continuación viene el esquema de una clase que mediante los SAVE POINTS y controlando el número de transacciones anidadas mediante la propiedad transactionDepth, consigue algo parecido a anidar transacciones y por lo tanto evita el error fatal antes mencionado.

Si tu clase extiende la clase PDO, el método execute puede ser reemplazado llamando simplemente a $this->exec() Como digo, esta clase es un esquema para entender la idea, no está pensada para funcionar directamente sino que el programador interesado en ella deberá adaptarla.

class Db { 
  static private $instance = null; 
  private $connection = null;
  protected $transactionDepth = 0; 
 
  private function __construct() { 
  }
  
  private function _connect() { 
     if ($this->connection === null) {
         try {
            /* Código para conectarse a la BD */
         } catch (PDOException $e) {
            echo "error pdo: ";
            echo $e->getMessage();
         }
      }

      return $this;
   }
   
   /* Nada que clonar en el patrón de diseño Singleton */
   private function __clone()
   {
     
   }

   static public function getInstance()
   {
      if (is_null(self::$instance)) {
         self::$instance = new self();
      }

      return self::$instance;
   }

   static public function closeConnection()
   {
      if (!self::$instance === null) {
         self::$instance = null;
      }
      if (isset(self::$connection)) {
         unset(self::$connection);
      }
   }

   public function getConnection()
   {
      $this->_connect();

      return $this->connection;
   }

   protected function prepare($query, $params = array())
   {
      $stmt = $this->getConnection()->prepare($query);
      if (is_array($params)) {
         foreach ($params as $param => $value) {
            if (is_bool($value)) {
               $type = PDO::PARAM_BOOL;
            } elseif ($value === null) {
               $type = PDO::PARAM_NULL;
            } elseif (is_integer($value)) {
               $type = PDO::PARAM_INT;
            } else {
               $type = PDO::PARAM_STR;
            }
            $stmt->bindValue(":$param", $value, $type);
         }
      }

      return $stmt;
   }

   public function execute($sql, $params = array())
   {
      $stmt = $this->prepare($sql, $params);
      $stmt->execute();
      $stmt->closeCursor();

      return $stmt->rowCount();
   }

   public function begin()
   {
      if ($this->transactionDepth == 0) {
         $this->getConnection()->beginTransaction();
      }else{
         $this->execute("SAVEPOINT LEVEL{$this->transactionDepth}");
      }
      $this->transactionDepth++;
   }

   public function commit()
   {
      $this->transactionDepth--;
      if ($this->transactionDepth == 0) {
         return $this->getConnection()->commit();
      }else{
         return $this->execute("RELEASE SAVEPOINT LEVEL{$this->transactionDepth}");
      }
   }

   public function rollback()
   {
      if ($this->transactionDepth == 0) {
         throw new PDOException("Ninguna transacción en curso para retroceder");
      }
      $this->transactionDepth--;
      if ($this->transactionDepth == 0) {
         return $this->getConnection()->rollback();
      }else{
         return $this->execute("ROLLBACK TO SAVEPOINT LEVEL{$this->transactionDepth}");
      }
   }
}

El código también está disponible en Github.

En PHP un sistema de plantillas es prescindible

Si bien es cierto que PHP hace muchos años que dejó de ser un sistema de plantillas tampoco me parece que necesite implementar un sistema de plantillas de forma imperiosa. En su origen, PHP era un sencillo lenguaje, cuyo intérprete programó en C Rasmus Lerdorf, destinado a implementar su página personal, de ahí el acrónimo Personal Home Page. Mucho ha llovido desde entonces y hace tiempo que PHP es un lenguaje orientado a objetos bastante potente con muchas librerías a su disposición. A pesar de ello, un motor de plantillas me parece una sobrecarga innecesaria y mucho menos me convence el razonamiento de querer dotar al lenguaje de una especie de aura de seriedad. En casos concretos hace más bien que mal, pero hay 2 razones por las que en muchos casos me parece prescindible.

Primera: Un sistema de templates añade una sobrecarga no despreciable. El entorno más frecuente para PHP son los servidores web compartidos que alojan diversas webs. Los costes de alojamiento son reducidos por la poca carga que supone PHP, tanto en tiempo de CPU como RAM consumida. Poner en esos servidores mastodontes como Symfony tiene su peaje. El patrón MVC es conveniente, la sobrecarga no, y la V de vista no implica usar plantillas. Más sobre esto en mi segunda razón.

Hace ya años que PHP es usado también en grandes aplicaciones web, con miles de usuarios conectados simultáneamente y cuyo alojamiento no alcanza en un sólo servidor sino que se requiere de una server farm. Trabajé en una empresa así y recuerdo que la competencia de dicha empresa había apostado por Java, un lenguaje más “serio” (más empleado a nivel corporativo). El gasto en servidores de esa empresa multiplicaba en varias veces el de la nuestra y cuando se llega a cierto volumen los costes dejan de ser despreciables y empiezan a marcar diferencias. Que PHP, a diferencia de Java, no necesite ingentes recursos para declarar un simple entero es una poderosa razón por la que barrió a Java de la web.

Segunda: Algunos argumentan que PHP es demasiado “parlanchín” para ser empleado en la vista pero no estoy de acuerdo. Puede (o no) que esto:

{foreach $foo as $bar}

Sea mejor que esto:

<?php foreach($foo as $bar){ ?>

Pero es idéntico a los “short tags” que PHP sigue permitiendo. Además, PHP dispone de una sintaxis alternativa para las estructuras de control:

<? foreach($foo as $bar): ?>

Esto también es objeto de crítica:

<?php echo htmlspecialchars($var, ENT_QUOTES, 'UTF-8') ?>

¿Por qué no usar esto?

<?=htmlspecialchars($var, ENT_QUOTES, 'UTF-8') ?>

Para que nadie sufra del síndrome metacarpiano podemos encapsular en algún helper esa función, como hace CakePHP:

<?=h($var) ?>

La función sería algo así:

function h($text)
{
return htmlspecialchars($text, ENT_COMPAT | ENT_HTML5, 'UTF-8');
}

Así mismo se pueden crear fácilmente otras funciones para agilizar la programación de las vistas.

También se argumenta que diseñadores y maquetadores no tienen por qué saber acerca de temas de seguridad como los ataques XSS y CSRF y que para que ellos puedan programar las vistas con seguridad debe implementarse un sistema de plantillas. En estos casos sí sería necesario usarlo y sólo añadiría lo siguiente: he conocido muchos diseñadores y no he visto ninguno interesado en programar en el lado servidor. Iría más lejos: no he conocido ninguno interesado en programar. A lo sumo, sí en aprender Flash en sus años buenos. Frecuentemente, ni tan siquiera Javascript, a pesar de incluir ficheros en sus diseños. Obviamente les interesan las funcionalidades que aporta pero en general no están interesados en saber cómo funciona. En la industria que yo conozco, programar las vistas acaba siendo trabajo también del programador y éste sí conoce (o debería) las implicaciones de seguridad. Si en la empresa los diseñadores sí están por la causa sí sería conveniente usar un sistema de templates.

Otras casos en los que puede ser interesante usarlo es cuando se requiere una sandbox con un susbset de las funciones propias y/o de PHP. Por ejemplo si el usuario desde el back end ha de poder acceder a arrays con datos y a ciertas funciones para transformar y operar esos datos. Para estos casos hay sistemas de templates como Twig que proporcionan una sandbox y sería razonable usarlos en vez de reinventar la rueda programando uno propio.

A pesar de no ser necesario un sistema de plantillas en la mayoría de aplicaciones web, ya consiguen hacer negocio con ellos. ¿Necesidad real o creada?