PHP中高效的JPEG图像调整大小

84

如何在PHP中高效地调整大图像的大小?

我目前使用GD函数imagecopyresampled来获取高分辨率图像,并将其清晰地调整为适合Web浏览的大小(大约700像素宽乘以700像素高)。

这对于小型照片(2 MB以下)非常有效,整个调整操作在服务器上不到一秒钟。然而,该网站最终将服务于可能上传大小高达10 MB的照片(或大小高达5000x4000像素的图像)的摄影师。

使用大型图像进行此类调整操作往往会大幅增加内存使用量(更大的图像可以将脚本的内存使用量提高到80 MB以上)。有没有办法使此调整操作更高效?我应该使用其他图像库,例如ImageMagick吗?

现在,调整大小的代码看起来像这样

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);
9个回答

48

人们认为ImageMagick更快。最好比较这两个库并测量它们。

  1. 准备1000张典型的图片。
  2. 编写两个脚本——一个用于GD,一个用于ImageMagick。
  3. 多次运行它们。
  4. 比较结果(总执行时间、CPU和I/O使用情况、结果图像质量)。

最好的东西不一定适合每个人。

此外,在我看来,ImageMagick具有更好的API接口。


2
在我使用过的服务器上,GD 经常会耗尽内存并崩溃,而 ImageMagick 从未出现过这种情况。 - Abhi Beckert
我非常不同意。我发现使用ImageMagick非常困难。对于大图片,我经常会遇到500服务器错误。虽然GD库会更早崩溃,但我们有时仅仅处理6Mb的图片,而500错误是最糟糕的。 - Single Entity

23

以下是我在项目中使用并且正常运行的来自php.net文档的片段:

<?
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/zh/function.imagecopyresampled.php#77679


9
我是这个函数的作者Tim Eckel。$quality + 1 是正确的,它用于避免一个像素宽的黑边框,而不是改变质量。此外,这个函数可以与imagecopyresampled插件兼容,因此有关语法的问题,请参阅imagecopyresampled命令,两者完全相同。 - Andomar
1
这个解决方案与问题中提出的方案相比有什么优势?你仍然使用GD库并调用相同的函数。 - Tomas
1
@Tomas,根据Tim Eckel的说法,这个示例中使用GD函数的方式是造成差异的关键。 - Xeoncross
2
@Tomas,实际上它也使用了imagecopyresized()。基本上,它首先将图像调整为可管理的大小(final dimensions乘以quality),然后对其进行重新采样,而不是简单地重新采样全尺寸图像。这可能会导致最终图像质量较低,但对于较大的图像,它比仅使用imagecopyresampled()更节省资源,因为重新采样算法默认只需处理3倍最终尺寸大小的图像,而不是全尺寸图像(特别是对于缩略图大小的照片进行调整大小时)。 - 0b10011
每当我在大图像上使用质量3时,会导致图像变黑。 - Alvaro
显示剩余3条评论

12

phpThumb 尽可能使用 ImageMagick 来提高速度(必要时退而使用 GD),并且似乎可以很好地缓存以减轻服务器负载。它很轻便,只需通过包含图形文件名和输出尺寸的 GET 查询调用 phpThumb.php 即可进行图像调整。因此,您可以尝试一下,看看它是否符合您的需求。


但是这似乎不是标准的PHP部分...所以它在大多数主机上不可用 :( - Tomas
1
我看这只是一个 PHP 脚本,你只需要安装 PHP GD 和 ImageMagick。 - Flo
这确实是一个PHP脚本,而不是需要安装的扩展,因此非常适合共享主机环境。当我尝试上传4000x3000尺寸且小于1MB的JPEG图像时,遇到了“已耗尽N字节允许的内存大小”的错误。使用phpThumb(以及ImageMagick)解决了这个问题,并且很容易将其整合到我的代码中。 - w5m

10

如果要处理较大的图片,建议使用libjpeg在ImageMagick中调整图像大小,在加载图像时能够显著地减少内存使用量并提高性能,而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();

9

从您的问题中看来,您似乎还不太了解GD库,我会分享一些我的经验,也许有点离题,但我认为对于像您这样的新手来说会很有帮助:

步骤1,验证文件。使用以下函数检查$_FILES['image']['tmp_name']文件是否为有效文件:

   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']);

第二步,获取文件格式使用finfo扩展库中的以下函数来检查文件(内容)的文件格式。你可能会问为什么不直接使用$_FILES["image"]["type"]来检查文件格式?因为它检查文件扩展名而不是文件内容,如果有人将原本名为world.png的文件重命名为world.jpg$_FILES["image"]["type"]将返回jpeg而不是png,所以$_FILES["image"]["type"]可能会返回错误的结果。

   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);

第三步,获取GD资源 从之前的内容中获取GD资源:

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

第四步,获取图片尺寸现在你可以使用以下简单的代码来获取图片的尺寸:

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

现在,让我们来看一下从原始图像中得到的变量:

       $contents, $format, $resource, $width, $height
       OK, lets move on
步骤5,计算调整大小的图像参数 这个步骤与您的问题有关,以下函数的目的是为GD函数imagecopyresampled()获取调整大小的参数,代码有点长,但它非常有效,它甚至有三个选项:拉伸、缩小和填充。 拉伸:输出图像的尺寸与您设置的新尺寸相同。不保持高度/宽度比例。 缩小:输出图像的尺寸不会超过您给出的新尺寸,并保持图像的高度/宽度比例。 填充:输出图像的尺寸将与您给出的新尺寸相同,如果需要,它将裁剪和调整大小图像,并保持图像的高度/宽度比例。这个选项是您在问题中需要的。
   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');

第六步,调整图像大小 使用上述步骤中获取的$args$width$height$format和$resource参数,将它们传入以下函数中,并获得调整大小后的新资源:

   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);

第七步,获取新内容,使用以下函数从新的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);

步骤8 获取扩展名,使用以下函数获取图像格式的扩展名(注意,图像格式并不等同于图像扩展名):

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

第九步:保存图像如果我们有一个名为Mike的用户,您可以执行以下操作,它将保存到与此php脚本相同的文件夹中:

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

步骤10 销毁资源 别忘了销毁GD资源!

imagedestroy($newresource);

你可以将所有的代码写入一个类中,并使用以下代码进行调用:
   public function __destruct() {
      @imagedestroy($this->resource);
   }

提示

我建议不要转换用户上传的文件格式,否则您会遇到很多问题。


4
我建议按以下方式进行操作:
  1. 对上传的文件执行getimagesize()操作,检查图像类型和大小
  2. 将任何小于700 x 700像素的上传JPEG图像“按原样”保存到目标文件夹中
  3. 使用GD库处理中等大小的图像(参见此文章的代码示例:使用PHP和GD库调整图像大小
  4. 对于大型图片,请使用ImageMagick。如果需要,您可以在后台使用ImageMagick。

要在后台使用ImageMagick,请将已上传的文件移动到临时文件夹,并安排一个CRON作业,将所有文件转换为jpeg格式并相应地调整其大小。请参见命令语法:imagemagick-command line processing

您可以提示用户文件已上传并计划进行处理。 CRON作业可以按特定间隔每天运行一次。 处理完源图像后,可以删除它以确保不会处理两次同一幅图像。


我看不出第三点的理由 - 对于中等大小的图像,为什么不也使用ImageMagick呢?那会更简化代码。 - Tomas
比起 cron,更好的办法是使用 inotifywait 的脚本,这样调整大小就可以立即开始,而不用等待 cron 作业启动。 - ColinM

3

ImageMagick是多线程的,因此它看起来比GD更快,但实际上使用的资源要多得多。如果您同时运行多个使用GD的PHP脚本,则它们会在简单操作方面击败ImageMagick。ExactImage比ImageMagick功能弱,但速度要快得多,虽然不能通过PHP使用,但您需要在服务器上安装它并通过exec运行。


3

我听说过Imagick库很厉害,可惜我无法在我的工作电脑和家里安装它(相信我,我在各种论坛上花费了数小时)。

后来,我决定尝试这个PHP类:

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

这很酷,我可以调整各种图像的大小(我也可以将它们转换为JPG格式)。


2

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接