PHP Imagick 在将 PDF 转换为图像时挂起整个服务器

PHP Imagick hangs the whole server when converting PDF to Image

我在 PHP 上使用扩展程序 Imagick 将 PDF 文件转换为图像,具体来说是 PNG 文件。 转换正在进行时,整个服务器总是挂起。我所做的是上传一堆 PDF 文件,遍历它及其页面以将其转换为图像。

这是代码。

foreach ($uploaded_file as $key => $value) {
  $upload_file_path = $upload_path.'/'.$value->name;
  $imagick = new Imagick();
  $imagick->setResourceLimit(6, 1);
  $imagick->setResolution(300, 300);
  $imagick->readImage($upload_file_path);
  $pages = $imagick->getNumberImages();

  for ($x=0; $x<$pages; $x++) {
    $imagick->readImage($upload_file_path.'['.$x.']');
    $imagick->setImageFormat('png');
    $imagick->writeImage($image_path.'/page-'.$x.'.png');
  }

  $imagick->clear();
  $imagick->destroy();
}

转换正在进行时我无法访问服务器上的其他站点,而且转换时间太长。请帮忙。谢谢!

根据 PDF 的内容,转换可能确实非常繁重。您对此无能为力。

您已经尝试将线程总数降低到 2,但仍然需要考虑内存、I/O 延迟和 CPU 资源——您不知道是否服务器可以使用两个以上的线程。

也许可以做的是尝试使用[=10=降低运行进程的I/O和CPU优先级] 和 ionice如果可用。您需要有一个合适的平台(Linux、BSD 或类似平台),并可以访问 niceionice 工具;并且您需要一个专用的可执行 PHP 进程(那么可能不是 FastCGI)并且能够查询其 PID。

在 Linux 系统上,您可能会尝试将转换外包给 ImageMagick 二进制文件,并将其包装在 niceionice 中以使其变得容易 - 但速度较慢。

另一种可能性是看看你是否可以使用 ghostscript 而不是 Imagick。

php-vips 将 pdf 文件转换为 png 文件的速度更快,您可以试试。

我给你做了一个示例程序:

#!/usr/bin/env php
<?php

require __DIR__ . '/vendor/autoload.php';

use Jcupitt\Vips;

for ($i = 1; $i < count($argv); $i++) {
  $image = Vips\Image::newFromFile($argv[$i]);
  $n_pages = $image->get("n-pages");
  echo($argv[$i] . " has " . $n_pages . " pages\n");

  for ($n = 0; $n < $n_pages; $n++) {
    echo("  rendering page " . $n . " ...\n");
    $page = Vips\Image::newFromFile($argv[$i], [
      "dpi" => 300,
      "page" => $n,
      # this enables image streaming
      "access" => "sequential"
    ]);
    $page->writeToFile($argv[$i] . "_page_" . $n . ".png");
  }
}

我可以运行这样:

$ /usr/bin/time -f %M:%e ../convert-vips.php nipguide.pdf 
nipguide.pdf has 58 pages
  rendering page 0 ...
...
  rendering page 57 ...
107808:31.72

因此它在 32 秒内制作了 58 个 png,并且最多需要 110mb 的内存。它不会创建任何临时文件 -- 110mb 涵盖所有内容。

png 是一种非常慢的文件格式。如果您另存为 jpg,则所有内容大约需要 6 秒。

我试过你的 imagick 代码的一个版本:

#!/usr/bin/env php
<?php

for ($i = 1; $i < count($argv); $i++) {
  $imagick = new Imagick();
  $imagick->setResourceLimit(6, 1);
  $imagick->setResolution(300, 300);
  $imagick->readImage($argv[$i]);
  $pages = $imagick->getNumberImages();
  echo($argv[$i] . " has " . $pages . " pages\n");

  for ($x = 0; $x < $pages; $x++) {
    echo("  rendering page " . $x . " ...\n");
    $imagick->readImage($argv[$i] . "[" . $x . "]");
    $imagick->setImageFormat("png");
    $imagick->writeImage($argv[$i] . "_page_" . $x . ".png");
  }

  $imagick->clear();
  $imagick->destroy();
} 

运行 我看到:

$ /usr/bin/time -f %M:%e ../convert-imagick.php nipguide.pdf 
nipguide.pdf has 58 pages
  rendering page 0 ...
...
  rendering page 57 ...
255640:223.26

所以 220 秒(慢了将近 7 倍)和 260mb 内存。内存使用并不是全部——在 300 DPI 下,imagick 将为每个页面在 /tmp 中创建一个 65mb 的文件,因此它总共需要大约 5gb 的存储空间。

我想分享我的发现和我认为对我的情况很有见地的解决方案。

我注意到每页创建一个新的 imagick 实例比尝试读取整个文件然后让它 1) 读取页数和 2) 让它迭代要快得多在所有页面上。

我只想要 PDF 文件的前 10 页。当我有一个 50 页的 PDF 文件时(大部分只有文本,只有 450KB),你阅读这个文件的方式真的很重要。

通过这种方式,它会简单地从第一页开始阅读,并尝试阅读到 10 页。如果少于 10 页,一旦 imagick 抛出错误,它就会意识到它已到达页面末尾并停止迭代。

<?php
$reachedLastPage = false;
for ($i = 0; $i <= 10 && empty($reachedLastPage); $i++) {
    $im = new imagick();
    $im->setResolution(300,300);
    try {
        $im->readimage($tempFile.'['.$i.']');
        if ($im->valid()) {
            $im->setImageBackgroundColor('white');
            $im->setImageAlphaChannel(Imagick::VIRTUALPIXELMETHOD_WHITE);
            $im->setImageCompression(imagick::COMPRESSION_JPEG);
            $im->setImageCompressionQuality(60);
            $im->setImageFormat('jpeg');

            $extraFile = microtime(true).'__pdfpage'.".".strtolower('jpg');
            $im->writeImage(rtrim($targetPath) . $extraFile);

            if (is_file(rtrim($targetPath) . $extraFile)) {
                $imageArray[] = $extraFile;
            }
        }
    }
    catch(ImagickException $e) {
        $reachedLastPage = true;
    }
    $im->clear();
    $im->destroy();
}
?>

为了更深入地了解我的其他尝试。

  1. 我先尝试阅读整个 pdf 文件,然后使用 getNumberImages()setIteratorIndex() 设置循环并阅读各个页面。对于 imagick 来说,阅读整个内容将花费很长时间,甚至还没有开始处理所有页面。

  2. 也可以使用 imagick 的轻量级实例来查看有多少页面,这比让 imagick 尝试读取没有页面的页面更简洁存在:

$im->pingImage($tempFile);
$nrOfPages = $im->getNumberImages();

然而这在我的测试中已经花费了10秒,仅仅为了阅读页数。这就是我最终采用上述方法的原因。