Cambio de tamaño de imagen JPEG eficiente en PHP

82

¿Cuál es la forma más eficiente de cambiar el tamaño de imágenes grandes en PHP?

Actualmente estoy usando la función GD imagecopyresampled para tomar imágenes de alta resolución y redimensionarlas limpiamente a un tamaño para visualización web (aproximadamente 700 píxeles de ancho por 700 píxeles de alto).

Esto funciona muy bien en fotos pequeñas (menos de 2 MB) y toda la operación de cambio de tamaño tarda menos de un segundo en el servidor. Sin embargo, el sitio eventualmente brindará servicios a los fotógrafos que pueden estar cargando imágenes de hasta 10 MB de tamaño (o imágenes de hasta 5000x4000 píxeles de tamaño).

Hacer este tipo de operación de cambio de tamaño con imágenes grandes tiende a aumentar el uso de la memoria por un margen muy grande (las imágenes más grandes pueden aumentar el uso de la memoria para el script por encima de 80 MB). ¿Hay alguna forma de hacer que esta operación de cambio de tamaño sea más eficiente? ¿Debería usar una biblioteca de imágenes alternativa como ImageMagick ?

En este momento, el código de cambio de tamaño se parece a esto

function makeThumbnail($sourcefile, $endfile, $thumbwidth, $thumbheight, $quality) {
    // Takes the sourcefile (path/to/image.jpg) and makes a thumbnail from it
    // and places it at endfile (path/to/thumb.jpg).

    // Load image and get image size.
    $img = imagecreatefromjpeg($sourcefile);
    $width = imagesx( $img );
    $height = imagesy( $img );

    if ($width > $height) {
        $newwidth = $thumbwidth;
        $divisor = $width / $thumbwidth;
        $newheight = floor( $height / $divisor);
    } else {
        $newheight = $thumbheight;
        $divisor = $height / $thumbheight;
        $newwidth = floor( $width / $divisor );
    }

    // Create a new temporary image.
    $tmpimg = imagecreatetruecolor( $newwidth, $newheight );

    // Copy and resize old image into new image.
    imagecopyresampled( $tmpimg, $img, 0, 0, 0, 0, $newwidth, $newheight, $width, $height );

    // Save thumbnail into a file.
    imagejpeg( $tmpimg, $endfile, $quality);

    // release the memory
    imagedestroy($tmpimg);
    imagedestroy($img);
maxsilver
fuente

Respuestas:

45

La gente dice que ImageMagick es mucho más rápido. En el mejor de los casos, compare ambas bibliotecas y mida eso.

  1. Prepara 1000 imágenes típicas.
  2. Escriba dos scripts: uno para GD y otro para ImageMagick.
  3. Ejecuta ambos un par de veces.
  4. Compare los resultados (tiempo total de ejecución, uso de CPU y E / S, calidad de imagen del resultado).

Algo que es lo mejor para todos los demás, no podría ser lo mejor para ti.

Además, en mi opinión, ImageMagick tiene una interfaz API mucho mejor.

Grzegorz Gierlik
fuente
2
En los servidores con los que he trabajado, GD a menudo se queda sin RAM y se bloquea, mientras que ImageMagick nunca lo hace.
Abhi Beckert
No puedo estar más en desacuerdo. Creo que imagemagick es una pesadilla para trabajar. Recibo 500 errores de servidor para imágenes grandes con frecuencia. Es cierto que la biblioteca de GD fallaría antes. Pero aún así, a veces solo hablamos de imágenes de 6Mb, y 500 errores son los peores.
Entidad única
20

Aquí hay un fragmento de los documentos de php.net que he usado en un proyecto y funciona bien:

<?
function fastimagecopyresampled (&$dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h, $quality = 3) {
    // Plug-and-Play fastimagecopyresampled function replaces much slower imagecopyresampled.
    // Just include this function and change all "imagecopyresampled" references to "fastimagecopyresampled".
    // Typically from 30 to 60 times faster when reducing high resolution images down to thumbnail size using the default quality setting.
    // Author: Tim Eckel - Date: 09/07/07 - Version: 1.1 - Project: FreeRingers.net - Freely distributable - These comments must remain.
    //
    // Optional "quality" parameter (defaults is 3). Fractional values are allowed, for example 1.5. Must be greater than zero.
    // Between 0 and 1 = Fast, but mosaic results, closer to 0 increases the mosaic effect.
    // 1 = Up to 350 times faster. Poor results, looks very similar to imagecopyresized.
    // 2 = Up to 95 times faster.  Images appear a little sharp, some prefer this over a quality of 3.
    // 3 = Up to 60 times faster.  Will give high quality smooth results very close to imagecopyresampled, just faster.
    // 4 = Up to 25 times faster.  Almost identical to imagecopyresampled for most images.
    // 5 = No speedup. Just uses imagecopyresampled, no advantage over imagecopyresampled.

    if (empty($src_image) || empty($dst_image) || $quality <= 0) { return false; }
    if ($quality < 5 && (($dst_w * $quality) < $src_w || ($dst_h * $quality) < $src_h)) {
        $temp = imagecreatetruecolor ($dst_w * $quality + 1, $dst_h * $quality + 1);
        imagecopyresized ($temp, $src_image, 0, 0, $src_x, $src_y, $dst_w * $quality + 1, $dst_h * $quality + 1, $src_w, $src_h);
        imagecopyresampled ($dst_image, $temp, $dst_x, $dst_y, 0, 0, $dst_w, $dst_h, $dst_w * $quality, $dst_h * $quality);
        imagedestroy ($temp);
    } else imagecopyresampled ($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h);
    return true;
}
?>

http://us.php.net/manual/en/function.imagecopyresampled.php#77679

Kevin
fuente
¿Sabes lo que pondrías por $ dst_x, $ dst_y, $ src_x, $ src_y?
JasonDavis
¿No deberías reemplazar $quality + 1con ($quality + 1)? Tal como está, solo está cambiando el tamaño con un píxel adicional inútil. ¿Dónde está el cheque para cortocircuito cuando $dst_w * $qualityes> $src_w?
Walf
8
Copiar / pegar de la edición sugerida: Este es Tim Eckel, el autor de esta función. El $ quality + 1 es correcto, se usa para evitar un borde negro de un píxel de ancho, no para cambiar la calidad. Además, esta función es compatible con el complemento imagecopyresampled, por lo que si tiene preguntas sobre la sintaxis, consulte el comando imagecopyresampled, es idéntico.
Andomar
¿cómo es esta solución mejor que la propuesta en la pregunta? todavía está utilizando la biblioteca GD con las mismas funciones.
TMS
1
@Tomas, en realidad, también lo está usando imagecopyresized(). Básicamente, primero se cambia el tamaño de la imagen a un tamaño manejable ( final dimensionsmultiplicado por quality), luego se vuelve a muestrear, en lugar de simplemente volver a muestrear la imagen a tamaño completo. Se puede dar lugar a una imagen final de menor calidad, pero utiliza muchos menos recursos para las imágenes más grandes que imagecopyresampled()solo como el algoritmo de remuestreo solamente tiene que lidiar con una imagen del tamaño de 3x las dimensiones finales por defecto, en comparación con la imagen de tamaño completo ( que puede ser mucho más grande, especialmente para fotos cuyo tamaño se cambia para miniaturas).
0b10011
12

phpThumb usa ImageMagick siempre que sea posible para la velocidad ( recurriendo a GD si es necesario) y parece almacenar en caché bastante bien para reducir la carga en el servidor. Es bastante ligero de probar (para cambiar el tamaño de una imagen, simplemente llame a phpThumb.php con una consulta GET que incluya el nombre del archivo gráfico y las dimensiones de salida), por lo que puede intentarlo para ver si satisface sus necesidades.

Fenry
fuente
pero esto no es parte del PHP estándar como parece ... por lo que no estará disponible en la mayoría de los alojamientos :(
TMS
1
me parece que es solo un script php, solo tienes que tener php gd e imagemagick
Flo
De hecho, es un script PHP en lugar de una extensión que debe instalar, por lo que es bueno para entornos de alojamiento compartido. Me estaba encontrando con el error "Se agotó el tamaño de memoria permitido de N bytes" al intentar cargar imágenes jpeg <1 MB con dimensiones de 4000x3000. El uso de phpThumb (y por lo tanto ImageMagick) resolvió el problema y fue muy fácil de incorporar a mi código.
w5m
10

Para imágenes más grandes, use libjpeg para cambiar el tamaño al cargar la imagen en ImageMagick y, por lo tanto, reducir significativamente el uso de memoria y mejorar el rendimiento, no es posible con GD.

$im = new Imagick();
try {
  $im->pingImage($file_name);
} catch (ImagickException $e) {
  throw new Exception(_('Invalid or corrupted image file, please try uploading another image.'));
}

$width  = $im->getImageWidth();
$height = $im->getImageHeight();
if ($width > $config['width_threshold'] || $height > $config['height_threshold'])
{
  try {
/* send thumbnail parameters to Imagick so that libjpeg can resize images
 * as they are loaded instead of consuming additional resources to pass back
 * to PHP.
 */
    $fitbyWidth = ($config['width_threshold'] / $width) > ($config['height_threshold'] / $height);
    $aspectRatio = $height / $width;
    if ($fitbyWidth) {
      $im->setSize($config['width_threshold'], abs($width * $aspectRatio));
    } else {
      $im->setSize(abs($height / $aspectRatio), $config['height_threshold']);
    }
    $im->readImage($file_name);

/* Imagick::thumbnailImage(fit = true) has a bug that it does fit both dimensions
 */
//  $im->thumbnailImage($config['width_threshold'], $config['height_threshold'], true);

// workaround:
    if ($fitbyWidth) {
      $im->thumbnailImage($config['width_threshold'], 0, false);
    } else {
      $im->thumbnailImage(0, $config['height_threshold'], false);
    }

    $im->setImageFileName($thumbnail_name);
    $im->writeImage();
  }
  catch (ImagickException $e)
  {
    header('HTTP/1.1 500 Internal Server Error');
    throw new Exception(_('An error occured reszing the image.'));
  }
}

/* cleanup Imagick
 */
$im->destroy();
Steve-o
fuente
9

De su pregunta, parece que es un poco nuevo en GD, compartiré algunas experiencias mías, tal vez esto esté un poco fuera del tema, pero creo que será útil para alguien nuevo en GD como usted:

Paso 1, validar archivo. Utilice la siguiente función para comprobar si el $_FILES['image']['tmp_name']archivo es válido:

   function getContentsFromImage($image) {
      if (@is_file($image) == true) {
         return file_get_contents($image);
      } else {
         throw new \Exception('Invalid image');
      }
   }
   $contents = getContentsFromImage($_FILES['image']['tmp_name']);

Paso 2, obtenga el formato del archivo Pruebe la siguiente función con la extensión finfo para verificar el formato del archivo (contenido). Diría que ¿por qué no lo usa $_FILES["image"]["type"]para verificar el formato de archivo? Debido a que SOLO verifica la extensión del archivo, no el contenido del archivo, si alguien cambia el nombre de un archivo originalmente llamado world.png a world.jpg , $_FILES["image"]["type"]devolverá jpeg no png, por lo que $_FILES["image"]["type"]puede devolver un resultado incorrecto.

   function getFormatFromContents($contents) {
      $finfo = new \finfo();
      $mimetype = $finfo->buffer($contents, FILEINFO_MIME_TYPE);
      switch ($mimetype) {
         case 'image/jpeg':
            return 'jpeg';
            break;
         case 'image/png':
            return 'png';
            break;
         case 'image/gif':
            return 'gif';
            break;
         default:
            throw new \Exception('Unknown or unsupported image format');
      }
   }
   $format = getFormatFromContents($contents);

Paso 3, Obtener recurso GD Obtener recurso GD de los contenidos que tenemos antes:

   function getGDResourceFromContents($contents) {
      $resource = @imagecreatefromstring($contents);
      if ($resource == false) {
         throw new \Exception('Cannot process image');
      }
      return $resource;
   }
   $resource = getGDResourceFromContents($contents);

Paso 4, obtenga la dimensión de la imagen Ahora puede obtener la dimensión de la imagen con el siguiente código simple:

  $width = imagesx($resource);
  $height = imagesy($resource);

Ahora, veamos qué variable obtuvimos de la imagen original entonces:

       $contents, $format, $resource, $width, $height
       OK, lets move on

Paso 5, calcule los argumentos de la imagen redimensionada Este paso está relacionado con su pregunta, el propósito de la siguiente función es obtener argumentos redimensionados para la función GD imagecopyresampled(), el código es un poco largo, pero funciona muy bien, incluso tiene tres opciones: estirar, encoger y llenar.

stretch : la dimensión de la imagen de salida es la misma que la nueva dimensión que estableciste. No mantendrá la relación altura / ancho.

encogimiento : la dimensión de la imagen de salida no excederá la nueva dimensión que proporcione y mantendrá la relación altura / ancho de la imagen.

Relleno : la dimensión de la imagen de salida será la misma que la nueva dimensión que proporcione, recortará y cambiará el tamaño de la imagen si es necesario, y mantendrá la relación altura / ancho de la imagen. Esta opción es lo que necesita en su pregunta.

   function getResizeArgs($width, $height, $newwidth, $newheight, $option) {
      if ($option === 'stretch') {
         if ($width === $newwidth && $height === $newheight) {
            return false;
         }
         $dst_w = $newwidth;
         $dst_h = $newheight;
         $src_w = $width;
         $src_h = $height;
         $src_x = 0;
         $src_y = 0;
      } else if ($option === 'shrink') {
         if ($width <= $newwidth && $height <= $newheight) {
            return false;
         } else if ($width / $height >= $newwidth / $newheight) {
            $dst_w = $newwidth;
            $dst_h = (int) round(($newwidth * $height) / $width);
         } else {
            $dst_w = (int) round(($newheight * $width) / $height);
            $dst_h = $newheight;
         }
         $src_x = 0;
         $src_y = 0;
         $src_w = $width;
         $src_h = $height;
      } else if ($option === 'fill') {
         if ($width === $newwidth && $height === $newheight) {
            return false;
         }
         if ($width / $height >= $newwidth / $newheight) {
            $src_w = (int) round(($newwidth * $height) / $newheight);
            $src_h = $height;
            $src_x = (int) round(($width - $src_w) / 2);
            $src_y = 0;
         } else {
            $src_w = $width;
            $src_h = (int) round(($width * $newheight) / $newwidth);
            $src_x = 0;
            $src_y = (int) round(($height - $src_h) / 2);
         }
         $dst_w = $newwidth;
         $dst_h = $newheight;
      }
      if ($src_w < 1 || $src_h < 1) {
         throw new \Exception('Image width or height is too small');
      }
      return array(
          'dst_x' => 0,
          'dst_y' => 0,
          'src_x' => $src_x,
          'src_y' => $src_y,
          'dst_w' => $dst_w,
          'dst_h' => $dst_h,
          'src_w' => $src_w,
          'src_h' => $src_h
      );
   }
   $args = getResizeArgs($width, $height, 150, 170, 'fill');

Paso 6, tamaño de imagen Uso $args, $width, $height, $formaty $ recursos llegamos desde arriba en la siguiente función y obtener el nuevo recurso de la imagen redimensionada:

   function runResize($width, $height, $format, $resource, $args) {
      if ($args === false) {
         return; //if $args equal to false, this means no resize occurs;
      }
      $newimage = imagecreatetruecolor($args['dst_w'], $args['dst_h']);
      if ($format === 'png') {
         imagealphablending($newimage, false);
         imagesavealpha($newimage, true);
         $transparentindex = imagecolorallocatealpha($newimage, 255, 255, 255, 127);
         imagefill($newimage, 0, 0, $transparentindex);
      } else if ($format === 'gif') {
         $transparentindex = imagecolorallocatealpha($newimage, 255, 255, 255, 127);
         imagefill($newimage, 0, 0, $transparentindex);
         imagecolortransparent($newimage, $transparentindex);
      }
      imagecopyresampled($newimage, $resource, $args['dst_x'], $args['dst_y'], $args['src_x'], $args['src_y'], $args['dst_w'], $args['dst_h'], $args['src_w'], $args['src_h']);
      imagedestroy($resource);
      return $newimage;
   }
   $newresource = runResize($width, $height, $format, $resource, $args);

Paso 7, obtenga nuevos contenidos , use la siguiente función para obtener contenidos del nuevo recurso GD:

   function getContentsFromGDResource($resource, $format) {
      ob_start();
      switch ($format) {
         case 'gif':
            imagegif($resource);
            break;
         case 'jpeg':
            imagejpeg($resource, NULL, 100);
            break;
         case 'png':
            imagepng($resource, NULL, 9);
      }
      $contents = ob_get_contents();
      ob_end_clean();
      return $contents;
   }
   $newcontents = getContentsFromGDResource($newresource, $format);

Paso 8 obtenga la extensión , use la siguiente función para obtener la extensión del formato de imagen (nota, el formato de imagen no es igual a la extensión de imagen):

   function getExtensionFromFormat($format) {
      switch ($format) {
         case 'gif':
            return 'gif';
            break;
         case 'jpeg':
            return 'jpg';
            break;
         case 'png':
            return 'png';
      }
   }
   $extension = getExtensionFromFormat($format);

Paso 9 guardar imagen Si tenemos un usuario llamado mike, puede hacer lo siguiente, se guardará en la misma carpeta que este script php:

$user_name = 'mike';
$filename = $user_name . '.' . $extension;
file_put_contents($filename, $newcontents);

Paso 10 destruya el recurso ¡No olvide destruir el recurso GD!

imagedestroy($newresource);

o puede escribir todo su código en una clase y simplemente usar lo siguiente:

   public function __destruct() {
      @imagedestroy($this->resource);
   }

CONSEJOS

Recomiendo no convertir el formato de archivo que carga el usuario, encontrará muchos problemas.

nuez
fuente
4

Te sugiero que trabajes algo en este sentido:

  1. Realice un getimagesize () en el archivo cargado para verificar el tipo y tamaño de la imagen
  2. Guarde cualquier imagen JPEG cargada de menos de 700x700px en la carpeta de destino "tal cual"
  3. Utilice la biblioteca GD para imágenes de tamaño mediano (consulte este artículo para ver un ejemplo de código: Cambiar el tamaño de las imágenes con PHP y la biblioteca GD )
  4. Utilice ImageMagick para imágenes grandes. Puede utilizar ImageMagick en segundo plano si lo prefiere.

Para usar ImageMagick en segundo plano, mueva los archivos cargados a una carpeta temporal y programe un trabajo CRON que "convierta" todos los archivos a jpeg y los cambie de tamaño en consecuencia. Consulte la sintaxis de comandos en: imagemagick-procesamiento de línea de comandos

Puede indicarle al usuario que el archivo está cargado y programado para ser procesado. El trabajo CRON se puede programar para que se ejecute diariamente en un intervalo específico. La imagen de origen se puede eliminar después del procesamiento para garantizar que una imagen no se procese dos veces.

Salman A
fuente
No veo ninguna razón para el punto 3: use GD para tamaño mediano. ¿Por qué no utilizar ImageMagick también para ellos? Eso simplificaría mucho el código.
TMS
Mucho mejor que cron sería un script que usa inotifywait para que el cambio de tamaño comience instantáneamente en lugar de esperar a que comience el trabajo cron.
ColinM
3

He escuchado cosas importantes sobre la biblioteca Imagick, desafortunadamente no pude instalarla en mi computadora de trabajo ni en casa (y créanme, pasé horas y horas en todo tipo de foros).

Después de esto, he decidido probar esta clase de PHP:

http://www.verot.net/php_class_upload.htm

Es genial y puedo cambiar el tamaño de todo tipo de imágenes (también puedo convertirlas a JPG).

alessioalex
fuente
3

ImageMagick es multiproceso, por lo que parece ser más rápido, pero en realidad usa muchos más recursos que GD. Si ejecutara varios scripts PHP en paralelo, todos usando GD, superarían a ImageMagick en velocidad para operaciones simples. ExactImage es menos potente que ImageMagick pero mucho más rápido, aunque no está disponible a través de PHP, tendrá que instalarlo en el servidor y ejecutarlo exec.

Alasdair
fuente