1

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.

rabudde
  • 7,498
  • 6
  • 53
  • 91
  • _"It's really important that all operations do work in memory only without reading files from disk or storing files on disk"_ ...why? – ADyson Dec 02 '19 at 10:37
  • Also, all your functions seem to involve reading the file from disk to begin with, so it's unclear what the difference is there anyway. If you don't want to read from the file, then you need to get the data into memory some other way...it's not clear what you're proposing to replace that with, or why it's relevant to this "optimisation". – ADyson Dec 02 '19 at 10:42
  • Also, php-vips is a library....if you want to optimise it, talk to the maintainers of the library - this community is unlikely to specialise in that sort of thing, unless you happen to get someone from the vips team answering here. Unless you believe there's some part of your code where you call the vips library which you think could be improved? Again it's unclear what that would be, or why you think it's important for vips to be used as the solution for this requirement, specifically. – ADyson Dec 02 '19 at 10:43
  • @ADyson I've used `file_get_contents()` for performance testing only. In production I get content via `curl`, convert the image and send it back to browser without file handling. – rabudde Dec 02 '19 at 10:50
  • Also, using in-memory processing is a legal thing. For me it's allowed to process images, but not to store images physically. – rabudde Dec 02 '19 at 10:56
  • I'm sure you could get round that problem by storing them in a temp folder and ensuring they get deleted as soon as you've finished processing them. The point is to ensure you don't store them beyond the time needed to process them. – ADyson Dec 02 '19 at 11:11
  • Believe me or not, it's forbidden to store them in any way on disk, even it's temporary. For my use case, it has to be processed in memory only. – rabudde Dec 02 '19 at 13:14
  • That's pretty restrictive! Ok. I googled this topic very briefly. According to https://developers.google.com/speed/webp/docs/cwebp, `using "-" as output name will direct output to 'stdout'`...so if that works, you should be able to capture the output back into a PHP variable (via the proc_open command), instead of it writing the results to a file. – ADyson Dec 02 '19 at 13:23

1 Answers1

7

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.

jcupitt
  • 10,213
  • 2
  • 23
  • 39
  • Great job so far. My problem is, version 8.4.5 is latest in Debian Stretch. If I want to use a newer one I have to compile it on my own, which is nothing I want to. And `thumbnail_buffer` is available starting from 8.5. But you approach sound promising, so I'll give it a try whenever a newer version will be available in repository. (On the other side it's great to hear that `cwebp` is so effective.) – rabudde Dec 08 '19 at 15:52
  • Here's a dockerfile that install current stable libvips + php-vips on debian9: https://github.com/jcupitt/docker-builds/blob/master/php-vips-debian9/Dockerfile – jcupitt Dec 09 '19 at 08:00