libvips/php-vips

Long JPEG image output

Dumra opened this issue · 9 comments

Dumra commented

Hey. I'm trying to change my watermarking image service from GD to libvips for increasing performance/decrease OOM errors.
The watermarking process really shows faster on a JPEG file with a huge resolution but I have a weird behavior with a long output process for the FullHD or bigger JPEG file resolution after watermarking. I have no clue how to fix it.

Here's my code.

        error_log("Start process " . LibVipsConversion::class);
        $startTime = microtime(true);
        $image = Vips\Image::newFromFile($filePath, [
            'access' => 'sequential',

        ]);

        $page_height = $image->height;
        $page_width = $image->width;

        $text = $this->text[0];
        //$text = str_pad($text, \strlen($text) + 20);

        $text_mask = Vips\Image::text($text, [
            'width' => $image->width,
            'dpi' => 102,
            'font' => 'DejaVuSans 14',
        ]);

        $foreground = [0, 0, 0, (int)(255 * ($this->opacity / 100))]; // 38 is ~15% of 255

        $overlay = $text_mask->ifthenelse($foreground, null, [
            'blend' => true
        ]);

        $margin = 20;
        $overlay = $overlay->embed(
            $margin,
            $margin,
            $overlay->width + 2 * $margin,
            $overlay->height + $margin,

        );

        $overlay = $overlay->copy(['interpretation' => 'srgb']);
        $overlay = $overlay->rotate(360 - 45);

        $num_repeats_x = (int)ceil($page_width / $overlay->width);
        $num_repeats_y = (int)ceil($page_height / $overlay->height);
        $overlay = $overlay->replicate($num_repeats_x, $num_repeats_y);

        $image = $image->composite2($overlay, 'over');
        error_log("Before output: " . (microtime(true) - $startTime) * 1000);
        $data = $image->jpegsave_buffer([
            'Q' => 80,           
        ]);

        error_log("Output ready: " . (microtime(true) - $startTime) * 1000);
        echo $data;
        error_log("Close: " . (microtime(true) - $startTime) * 1000);

Output FullHD file:

│ NOTICE: PHP message: Before output: 46.720027923584 
│ NOTICE: PHP message: Output ready: 248.31509590149
│ NOTICE: PHP message: Close: 249.15599822998

Output 4k file:

│ NOTICE: PHP message: Before output: 51.825046539307 
│ NOTICE: PHP message: Output ready: 514.9199962616
│ NOTICE: PHP message: Close: 517.19999313354

So it's 200ms for FullHD, and 500ms for 4k images.

Am I doing something wrong? Should I add some options into jpegsave_buffer method call?

My env:
Debian Bookworm
PHP 8.3
libvips42t64/testing,now 8.15.2-1+b1 amd64 [installed,automatic]
"jcupitt/vips": "^2.4",

Thanks.

Hello @Dumra,

Here's a watermark demo:

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

require __DIR__ . '/vendor/autoload.php';
use Jcupitt\Vips;

// Vips\Config::setLogger(new Vips\DebugLogger());
// Vips\Config::cacheSetMax(0);

if (count($argv) != 4) {
    echo("usage: ./watermark-text.php input output \"some text\"\n");
    exit(1);
}

$input_filename = $argv[1];
$output_filename = $argv[2];
$message = $argv[3];

function watermark($image, $message)
{
    $text = Vips\Image::text($message, [
      'width' => min(300, $image->width),
      'height' => min(300, $image->height),
      'align' => 'centre',
      'rgba' => true
    ]);
    
    // scale the alpha down to make it semi-transparent, and rotate by 45
    // degrees
    $text = $text
        ->multiply([1, 1, 1, 0.3])
        ->rotate(45)
        ->copyMemory();
    
    // replicate and crop to match the size of the image
    $text = $text
        ->replicate(1 + $image->width / $text->width,
            1 + $image->height / $text->height)
        ->crop(0, 0, $image->width, $image->height);
    
    // and overlay on the image
    return $image->composite($text, "over");
}

for ($i = 0; $i < 100; $i++) {
    $image = Vips\Image::newFromFile($input_filename, [
      'access' => 'sequential',
    ]);

    $image = watermark($image, $message);

    $image->writeToFile($output_filename);
}

On this laptop I see:

$ vips crop ~/pics/theo.jpg x.jpg 0 0 3280 2160
$ time ./watermark-text.php x.jpg y.jpg "hello world"

real	0m7.313s
user	0m46.295s
sys	0m1.862s

So it's doing a 4k image in about 70ms.

... for speed, the big changes over your code are:

  • Use the rgba option to Text rather than building the alpha yourself. You can colour the text with pangomarkup, eg. "<span foreground='red'>hello world</span>"
  • use CopyMemory() to reuse the text and rotate (otherwise replicate will recompute it each time)
Dumra commented

heh.
tested your code in my k8s docker env. pod with 3 CPU and 2GB RAM

real	0m14.736s
use  0m46.768s
sys	0m1.534s

not as great as your, but ur optimization works for me. Appreciate it.
file attached.

file_9489620

Dumra commented

Unfortunately, my GD script anyway works faster.

Oh, interesting. Would you mind posting the gd script? I'd be curious to see.

Dumra commented

Sure thing.

<?php

declare(strict_types=1);

if (count($argv) != 3) {
    echo("usage: ./watermark-text.php input output \n");
    exit(1);
}

$input_filename = $argv[1];
$output_filename = $argv[2];

function watermark($sourceImage, $outputFile)
{
    $fontFile = '/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf';
    $fontSize = 15;
    $width = imagesx($sourceImage);
    $height = imagesy($sourceImage);

    $maxDiagonal = maxDiagonalLength($width, $height);
    $numberOfLines = 10;
    $interlineSpacing = $maxDiagonal / $numberOfLines;

    $transparentColor = imagecolorallocatealpha($sourceImage, 0, 0, 0, 50);
    $watermarkLines = getAllAnnotationLines($fontSize, $fontFile, $maxDiagonal, [
        'mail@test.com - 192.168.1.1',
        'CONFIDENTIAL INFORMATION - DO NOT DISTRIBUTE',
    ]);

    $yPos = 50;
    while ($numberOfLines >= 0) {
        foreach ($watermarkLines as $line) {
            imagefttext($sourceImage, $fontSize, 45, 0, $yPos, $transparentColor, $fontFile, $line);
            $yPos += (int)$interlineSpacing;
            $numberOfLines--;
        }
    }

    imagejpeg($sourceImage, $outputFile, 80);
    imagedestroy($sourceImage);
}

function maxDiagonalLength($width, $height): float
{
    $longestSide = max($width, $height);

    return sqrt(2 * ($longestSide ** 2));
}
function getAllAnnotationLines($fontSize, $font_file, float $diagonal, array $textLines): array
{
    return array_map(
        function ($line) use ($fontSize, $font_file, $diagonal) {
            $textPadded = str_pad($line, \strlen($line) + 20);
            $type_space = imagettfbbox($fontSize, 0, $font_file, $textPadded);
            $textWidth = abs($type_space[4] - $type_space[0]);

            return str_repeat($textPadded, (int)ceil($diagonal / (!empty($textWidth) ? $textWidth : 0.00001)));
        },
        $textLines
    );
}

for ($i = 0; $i < 100; $i++) {
    $data = file_get_contents($input_filename);
    $sourceImage = imagecreatefromstring($data);
    watermark($sourceImage, $output_filename);

}

I tweaked the libvips version to match the GD output:

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

require __DIR__ . '/vendor/autoload.php';
use Jcupitt\Vips;

// Vips\Config::setLogger(new Vips\DebugLogger());
// Vips\Config::cacheSetMax(0);

$input_filename = $argv[1];
$output_filename = $argv[2];
    
function watermark($image, $message)
{   
    $text = Vips\Image::text($message, [
      'font' => 'sans 20',
      'align' => 'centre',
    ]);
    
    // scale the alpha down to make it semi-transparent, and rotate by 45
    // degrees
    $text = $text
        ->linear(0.5, 0, ['uchar' => true])
        ->rotate(-45)
        ->copyMemory();
    
    // replicate and crop to match the size of the image
    $text = $text
        ->replicate(1 + $image->width / $text->width,
            1 + $image->height / $text->height)
        ->crop(0, 0, $image->width, $image->height);
    
    return $text->ifthenelse(0, $image, ['blend' => true]);
}

for ($i = 0; $i < 100; $i++) {
    $image = Vips\Image::newFromFile($input_filename, [
      'access' => 'sequential',
    ]);

    $image = watermark($image, 
"mail@test.com - 192.168.1.1





CONFIDENTIAL INFORMATION - DO NOT DISTRIBUTE");

    $image->writeToFile($output_filename);
}

And sped it up slightly:

  • the libvips version was fitting the text to a box rather than just using a hardwired fontsize, so I changed it to always use sans 20
  • the composite operation is relatively slow -- I swapped it for ifthenelse
  • I scaled the alpha in 8 bits rather than float
  • you can shrink the libvips threadpool for a slight speedup (there's not that much parallelism here)

I now see:

$ export VIPS_CONCURRENCY=2
$ time ./watermark-text-libvips.php ~/pics/k2.jpg x-vips.jpg

real	0m3.258s
user	0m6.078s
sys	0m0.361s
$ time ./watermark-text-gd.php ~/pics/k2.jpg x-gd.jpg

real	0m4.230s
user	0m3.836s
sys	0m0.391s

So libvips is a little quicker, but it's not impressive.

Dumra commented

@jcupitt Looks great. Thanks. Will test.

Dumra commented

@jcupitt Appreciate ur help.
Close this one.