Search code examples
phpphp-7webpvips

How to optimize VIPS performance in PHP


I want to convert images held as strings in variables as fast as possible to WebP format while shrinking larger images but do not enlarge smaller images. The base system is a Debian 9.9 with PHP 7.3. I've tried to measure speed for the following techniques: imagejpeg, imagewebp, using cwep and php-vips. I used the following code:

$jpeg = function() use ($image) {
    $old_image = @imagecreatefromstring($image);
    $old_width = (int)@imagesx($old_image);
    $old_height = (int)@imagesy($old_image);
    $new_width = 1920;
    $new_width = min($old_width, $new_width);
    $ratio = $new_width / $old_width;
    $new_height = $old_height * $ratio;
    $new_image = imagecreatetruecolor($new_width, $new_height);
    imagecopyresampled($new_image, $old_image, 0, 0, 0, 0, $new_width, $new_height, $old_width, $old_height);
    ob_start();
    imagejpeg($new_image, NULL, 75);
    $image = ob_get_clean();
};
$webp = function() use ($image) {
    $old_image = @imagecreatefromstring($image);
    $old_width = (int)@imagesx($old_image);
    $old_height = (int)@imagesy($old_image);
    $new_width = 1920;
    $new_width = min($old_width, $new_width);
    $ratio = $new_width / $old_width;
    $new_height = $old_height * $ratio;
    $new_image = imagecreatetruecolor($new_width, $new_height);
    imagecopyresampled($new_image, $old_image, 0, 0, 0, 0, $new_width, $new_height, $old_width, $old_height);
    ob_start();
    imagewebp($new_image, NULL, 75);
    $image = ob_get_clean();
};
$convert = function(string $image, int $width, int $height) {
    $cmd = sprintf('cwebp -m 0 -q 75 -resize %d %d -o - -- -', $width, $height);
    $fd = [
        0 => [ 'pipe', 'r' ], // stdin is a pipe that the child will read from
        1 => [ 'pipe', 'w' ], // stdout is a pipe that the child will write to
        2 => [ 'pipe', 'w' ], // stderr is a pipe that the child will write to
    ];
    $process = proc_open($cmd, $fd, $pipes, NULL, NULL);
    if (is_resource($process)) {
        fwrite($pipes[0], $image);
        fclose($pipes[0]);
        $webp = stream_get_contents($pipes[1]);
        fclose($pipes[1]);
        $result = proc_close($process);
        if ($result === 0 && strlen($webp)) {
            return $webp;
        }
    }
    return FALSE;
};
$cwebp = function() use ($image, $convert) {
    $old_image = @imagecreatefromstring($image);
    $old_width = (int)@imagesx($old_image);
    $old_height = (int)@imagesy($old_image);
    $new_width = 1920;
    $new_width = min($old_width, $new_width);
    $ratio = $new_width / $old_width;
    $new_height = $old_height * $ratio;
    $image = $convert($image, $new_width, $new_height);
};
$vips = function() use ($image) {
    $image = Vips\Image::newFromBuffer($image);
    $old_width = (int)$image->get('width');
    $old_height = (int)$image->get('height');
    $new_width = 1920;
    $new_width = min($old_width, $new_width);
    $ratio = $new_width / $old_width;
    // $new_height = $old_height * $ratio;
    $image = $image->resize($ratio);
    $image = $image->writeToBuffer('.webp[Q=75]');
};

I've called $jpeg(), $webp(), $cwebp() and $vips() ten times in a loop and runtime in seconds is:

JPEG: 0.65100622177124
WEBP: 1.4864070415497
CWEBP: 0.52562999725342
VIPS: 1.1211001873016

So calling cwebp CLI tool seems to be the fastest way, that is surprising. I've read many times that vips is a extremly fast tool (mostly faster than imagemagick), so I'd like to focus on vips.

Can anyone help me to optimize $vips() for better performance? Maybe there are some options for writeToBuffer() or resize() which are not known to me. It's really important that all operations do work in memory only without reading files from disk or storing files on disk.


Solution

  • For speed, don't use resize, use thumbnail_buffer. It combines open and resize in a single operation, so it can take advantage of things like shrink-on-load. You can get a huge speedup, depending on the image formats and sizes.

    You can match your cwebp settings with something like:

    $vips = function() use ($source_bytes) {
        $image = Vips\Image::thumbnail_buffer($source_bytes, 1920);
        $dest_bytes = $image->writeToBuffer('.webp', [
            'Q' => 75,
            'reduction-effort' => 0,
            'strip' => TRUE
        ]);
    };
    

    libvips seems slower at straight webp compression. I tried:

    $ time cwebp -m 0 -q 75 ~/pics/k2.jpg -o x.webp
    real    0m0.102s
    user    0m0.087s
    sys 0m0.012s
    

    The matching vips command would be:

    $ time vips copy ~/pics/k2.jpg x.webp[Q=75,reduction-effort=0,strip]
    real    0m0.144s
    user    0m0.129s
    sys 0m0.024s
    

    You can see it's perhaps 30% slower. There's no image processing here, just calls into libjpeg and libwebp, so cwebp must be using some libwebp save optimisation that libvips is not. They ran at the same speed a year ago -- I should read cwebp.c again and see what's changed.

    If you do some processing as well, then libvips becomes faster. I tried with a 10,000 x 10,000 pixel image and resize:

    $ /usr/bin/time -F %M:%e cwebp -m 0 -q 75 ~/pics/wtc.jpg -resize 1920 1920 -o x.webp
    618716:1.37
    

    620mb of memory and 1.4s, versus:

    $ /usr/bin/time -f %M:%e vipsthumbnail ~/pics/wtc.jpg -s 1920 -o x.webp[Q=75,reduction-effort=0,strip]
    64024:1.08
    

    64mb of memory and 1.1s.

    So libvips comes out faster and needs less memory, despite being slower at webp compression, because it can resize quickly. libvips is doing a higher-quality resize as well (adaptive lanczos3), versus simple cubic (I think) for cwebp.