Skip to content

Commit

Permalink
Refactor WebP conversion: add cwebp and imagick engines
Browse files Browse the repository at this point in the history
- Extract WebP conversion logic into separate converter classes
- Add CWebPConverter using cwebp binary for high-quality conversion
- Add ImagickConverter for servers with ImageMagick support
- Keep GDConverter as default fallback option
  • Loading branch information
mrfsrf committed Jan 14, 2025
1 parent d7b3b80 commit a5e4d4b
Show file tree
Hide file tree
Showing 6 changed files with 191 additions and 29 deletions.
44 changes: 44 additions & 0 deletions src/Image/Converter/CWebPConverter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

namespace Timber\Image\Converter;

use Timber\Helper;

/**
* cwebp CLI implementation
*/
class CWebPConverter implements IConverter
{
private $binary_path;

public function __construct(
private $quality
) {
$this->binary_path = \apply_filters('webp_cwebp_path', '/usr/local/bin/cwebp');
}

public function convert($load_filename, $save_filename)
{
if (!\file_exists($this->binary_path)) {
Helper::error_log('cwebp binary not found at: ' . $this->binary_path);
return false;
}

$command = \sprintf(
'%s -q %d %s -o %s 2>&1',
\escapeshellcmd($this->binary_path),
$this->quality,
\escapeshellarg((string) $load_filename),
\escapeshellarg((string) $save_filename)
);

\exec($command, $output, $return_var);

if ($return_var !== 0) {
Helper::error_log('cwebp conversion failed: ' . \implode(' ', $output));
return false;
}

return true;
}
}
48 changes: 48 additions & 0 deletions src/Image/Converter/GDConverter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

namespace Timber\Image\Converter;

use Timber\Helper;

/**
* Original GD implementation
*/
class GDConverter implements IConverter
{
public function __construct(
private $quality
) {
}

public function convert($load_filename, $save_filename)
{
$ext = \wp_check_filetype($load_filename);
if (isset($ext['ext'])) {
$ext = $ext['ext'];
}
$ext = \strtolower((string) $ext);
$ext = \str_replace('jpg', 'jpeg', $ext);

$imagecreate_function = 'imagecreatefrom' . $ext;
if (!\function_exists($imagecreate_function)) {
return false;
}

$input = $imagecreate_function($load_filename);

if ($input === false) {
return false;
}

if (!\imageistruecolor($input)) {
\imagepalettetotruecolor($input);
}

if (!\function_exists('imagewebp')) {
Helper::error_log('The function imagewebp does not exist on this server to convert image to ' . $save_filename . '.');
return false;
}

return \imagewebp($input, $save_filename, $this->quality);
}
}
11 changes: 11 additions & 0 deletions src/Image/Converter/IConverter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace Timber\Image\Converter;

/**
* Interface for WebP converters
*/
interface IConverter
{
public function convert($load_filename, $save_filename);
}
36 changes: 36 additions & 0 deletions src/Image/Converter/ImagickConverter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

namespace Timber\Image\Converter;

use Exception;
use Imagick;
use Timber\Helper;

/**
* ImageMagick implementation
*/
class ImagickConverter implements IConverter
{
public function __construct(
private $quality
) {
}

public function convert($load_filename, $save_filename)
{
if (!\class_exists('Imagick')) {
Helper::error_log('Imagick is not installed on this server.');
return false;
}

try {
$image = new Imagick($load_filename);
$image->setImageFormat('webp');
$image->setImageCompressionQuality($this->quality);
return $image->writeImage($save_filename);
} catch (Exception $e) {
Helper::error_log('Imagick conversion failed: ' . $e->getMessage());
return false;
}
}
}
56 changes: 27 additions & 29 deletions src/Image/Operation/ToWebp.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

namespace Timber\Image\Operation;

use Timber\Helper;
use Timber\Image\Converter\CWebPConverter;
use Timber\Image\Converter\GDConverter;
use Timber\Image\Converter\ImagickConverter;
use Timber\Image\Operation as ImageOperation;
use Timber\ImageHelper;

Expand All @@ -13,12 +15,15 @@
*/
class ToWebp extends ImageOperation
{
private $converter_engine;

/**
* @param string $quality ranges from 0 (worst quality, smaller file) to 100 (best quality, biggest file)
*/
public function __construct(
private $quality
) {
$this->converter_engine = \apply_filters('webp_converter_engine', 'gd');
}

/**
Expand All @@ -32,6 +37,25 @@ public function filename($src_filename, $src_extension = 'webp')
return $new_name;
}

/**
* Factory method to get the appropriate converter instance
*
* @return IConverter
*/
private function get_converter()

Check failure on line 45 in src/Image/Operation/ToWebp.php

View workflow job for this annotation

GitHub Actions / PHP static analysis

Method Timber\Image\Operation\ToWebp::get_converter() has invalid return type Timber\Image\Operation\IConverter.
{
return match ($this->converter_engine) {
'imagick' => new ImagickConverter($this->quality),
'cwebp' => new CWebPConverter($this->quality),
default => new GDConverter($this->quality)
};
}

public function get_active_converter_class(): string
{
return $this->get_converter()::class;

Check failure on line 56 in src/Image/Operation/ToWebp.php

View workflow job for this annotation

GitHub Actions / PHP static analysis

Access to constant class on an unknown class Timber\Image\Operation\IConverter.
}

/**
* Performs the actual image manipulation,
* including saving the target file.
Expand All @@ -52,33 +76,7 @@ public function run($load_filename, $save_filename)
return false;
}

$ext = \wp_check_filetype($load_filename);
if (isset($ext['ext'])) {
$ext = $ext['ext'];
}
$ext = \strtolower((string) $ext);
$ext = \str_replace('jpg', 'jpeg', $ext);

$imagecreate_function = 'imagecreatefrom' . $ext;
if (!\function_exists($imagecreate_function)) {
return false;
}

$input = $imagecreate_function($load_filename);

if ($input === false) {
return false;
}

if (!\imageistruecolor($input)) {
\imagepalettetotruecolor($input);
}

if (!\function_exists('imagewebp')) {
Helper::error_log('The function imagewebp does not exist on this server to convert image to ' . $save_filename . '.');
return false;
}

return \imagewebp($input, $save_filename, $this->quality);
$converter = $this->get_converter();
return $converter->convert($load_filename, $save_filename);

Check failure on line 80 in src/Image/Operation/ToWebp.php

View workflow job for this annotation

GitHub Actions / PHP static analysis

Call to method convert() on an unknown class Timber\Image\Operation\IConverter.
}
}
25 changes: 25 additions & 0 deletions tests/test-timber-image-towebp.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
<?php

use Timber\Image\Converter;
use Timber\Image\Operation\ToWebp;

class TestTimberImageToWEBP extends Timber_UnitTestCase
{
public function set_up()
Expand All @@ -19,6 +22,28 @@ public function testTIFtoWEBP()
$this->assertEquals($filename, $str);
}

public function testCwebpTIFtoWEB()
{
add_filter('webp_converter_engine', fn ($engine) => 'cwebp');
add_filter('webp_cwebp_path', fn ($path) => '/opt/homebrew/bin/cwebp');

$converter = new ToWebp(80);
$converter_class = $converter->get_active_converter_class();
$filename = TestTimberImage::copyTestAttachment('white-castle.tif');
$str = Timber::compile_string('{{file|towebp}}', [
'file' => $filename,
]);
$this->assertTrue(Converter\CWebPConverter::class === $converter_class);

$upload_dir = wp_upload_dir();
$site_url = get_site_url();
$relative_path = str_replace($upload_dir['basedir'], '', $filename);
$relative_path = dirname($relative_path) . '/' . pathinfo($relative_path, PATHINFO_FILENAME) . '.webp';
$expected_url = $site_url . '/wp-content/uploads' . $relative_path;

$this->assertEquals($expected_url, $str);

Check failure on line 44 in tests/test-timber-image-towebp.php

View workflow job for this annotation

GitHub Actions / PHP 8.1 | WP 6.2 | highest

Failed asserting that two strings are equal.

Check failure on line 44 in tests/test-timber-image-towebp.php

View workflow job for this annotation

GitHub Actions / PHP 8.1 | WP latest | lowest

Failed asserting that two strings are equal.

Check failure on line 44 in tests/test-timber-image-towebp.php

View workflow job for this annotation

GitHub Actions / PHP 8.2 | WP latest | highest

Failed asserting that two strings are equal.

Check failure on line 44 in tests/test-timber-image-towebp.php

View workflow job for this annotation

GitHub Actions / PHP 8.1 | WP 6.2 | lowest

Failed asserting that two strings are equal.

Check failure on line 44 in tests/test-timber-image-towebp.php

View workflow job for this annotation

GitHub Actions / PHP 8.2 | WP 6.2 | lowest

Failed asserting that two strings are equal.

Check failure on line 44 in tests/test-timber-image-towebp.php

View workflow job for this annotation

GitHub Actions / PHP 8.2 | WP latest | lowest

Failed asserting that two strings are equal.

Check failure on line 44 in tests/test-timber-image-towebp.php

View workflow job for this annotation

GitHub Actions / PHP 8.2 | WP 6.2 | highest

Failed asserting that two strings are equal.

Check failure on line 44 in tests/test-timber-image-towebp.php

View workflow job for this annotation

GitHub Actions / PHP 8.1 (Imagick) | WP latest | highest

Failed asserting that two strings are equal.

Check failure on line 44 in tests/test-timber-image-towebp.php

View workflow job for this annotation

GitHub Actions / PHP 8.1 (with coverage) | WP latest | highest

Failed asserting that two strings are equal.

Check failure on line 44 in tests/test-timber-image-towebp.php

View workflow job for this annotation

GitHub Actions / PHP 8.1 | WP latest (MS) | lowest

Failed asserting that two strings are equal.

Check failure on line 44 in tests/test-timber-image-towebp.php

View workflow job for this annotation

GitHub Actions / PHP 8.2 | WP trunk | highest

Failed asserting that two strings are equal.

Check failure on line 44 in tests/test-timber-image-towebp.php

View workflow job for this annotation

GitHub Actions / PHP 8.2 | WP latest (MS) | highest

Failed asserting that two strings are equal.

Check failure on line 44 in tests/test-timber-image-towebp.php

View workflow job for this annotation

GitHub Actions / PHP 8.2 | WP latest (MS) | lowest

Failed asserting that two strings are equal.

Check failure on line 44 in tests/test-timber-image-towebp.php

View workflow job for this annotation

GitHub Actions / PHP 8.1 | WP latest | highest

Failed asserting that two strings are equal.

Check failure on line 44 in tests/test-timber-image-towebp.php

View workflow job for this annotation

GitHub Actions / PHP 8.1 | WP latest (MS) | highest

Failed asserting that two strings are equal.

Check failure on line 44 in tests/test-timber-image-towebp.php

View workflow job for this annotation

GitHub Actions / PHP 8.2 | WP 6.2 (MS) | highest

Failed asserting that two strings are equal.

Check failure on line 44 in tests/test-timber-image-towebp.php

View workflow job for this annotation

GitHub Actions / PHP 8.1 | WP 6.2 (MS) | lowest

Failed asserting that two strings are equal.

Check failure on line 44 in tests/test-timber-image-towebp.php

View workflow job for this annotation

GitHub Actions / PHP 8.3 | WP trunk | highest

Failed asserting that two strings are equal.

Check failure on line 44 in tests/test-timber-image-towebp.php

View workflow job for this annotation

GitHub Actions / PHP 8.2 | WP 6.2 (MS) | lowest

Failed asserting that two strings are equal.

Check failure on line 44 in tests/test-timber-image-towebp.php

View workflow job for this annotation

GitHub Actions / PHP 8.1 | WP 6.2 (MS) | highest

Failed asserting that two strings are equal.
}

public function testPNGtoWEBP()
{
$filename = TestTimberImage::copyTestAttachment('flag.png');
Expand Down

0 comments on commit a5e4d4b

Please sign in to comment.