Long JPEG image output
Dumra opened this issue · 9 comments
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
rgbaoption toTextrather 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)
Unfortunately, my GD script anyway works faster.
Oh, interesting. Would you mind posting the gd script? I'd be curious to see.
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
compositeoperation is relatively slow -- I swapped it forifthenelse - 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.
