Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php
/**
* Helper functions used for checking Imagick AVIF transparency support.
*
* @package performance-lab
* @since n.e.x.t
*/

// @codeCoverageIgnoreStart
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
// @codeCoverageIgnoreEnd

/**
* Callback for webp_enabled test.
*
* @since n.e.x.t
*
* @return array{label: string, status: string, badge: array{label: string, color: string}, description: string, actions: string, test: string} Result.
*/
function perflab_imagick_avif_transparency_supported_test(): array {
$result = array(
'label' => __( 'Your site supports AVIF image format transparency with ImageMagick', 'performance-lab' ),
'status' => 'good',
'badge' => array(
'label' => __( 'Performance', 'performance-lab' ),
'color' => 'blue',
),
'description' => sprintf(
'<p>%s</p>',
__( 'Older versions of ImageMagick do not support transparency in AVIF images, which can result in loss of transparency when uploading AVIF files.', 'performance-lab' )
),
'actions' => sprintf(
'<p><strong>%s</strong></p>',
__( 'Your ImageMagick installation supports AVIF transparency.', 'performance-lab' )
),
'test' => 'is_imagick_avif_transparency_supported_enabled',
);

if ( ! perflab_imagick_avif_transparency_supported() ) {
$result['status'] = 'recommended';
$result['label'] = __( 'Your site does not support AVIF transparency', 'performance-lab' );
$result['actions'] = sprintf(
'<p>%s</p>',
__( 'Update ImageMagick to the latest version by contacting your hosting provider.', 'performance-lab' )
);
}

return $result;
}

/**
* Checks if Imagick has AVIF transparency support.
*
* @since n.e.x.t
*
* @return bool True if Imagick has AVIF transparency support, false otherwise.
*/
function perflab_imagick_avif_transparency_supported(): bool {
if ( extension_loaded( 'imagick' ) && class_exists( 'Imagick' ) ) {
$imagick_version = Imagick::getVersion();
if ( (bool) preg_match( '/\d+(?:\.\d+)+(?:-\d+)?/', $imagick_version['versionString'], $matches ) ) {
$imagick_version = $matches[0];
} else {
$imagick_version = $imagick_version['versionString'];
}
return version_compare( $imagick_version, '7.0.25', '>=' );
}

return false;
}
Copy link
Contributor Author

@b1ink0 b1ink0 Nov 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make more sense to move this Site Health test to the Modern Image Formats plugin?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just seeing this comment. I think it makes sense to move all Site Health tests for images to the plugin. See #1781

Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php
/**
* Hook callbacks used for checking Imagick AVIF transparency support.
*
* @package performance-lab
* @since n.e.x.t
*/

// @codeCoverageIgnoreStart
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
// @codeCoverageIgnoreEnd

/**
* Adds tests to site health.
*
* @since n.e.x.t
*
* @param array{direct: array<string, array{label: string, test: string}>} $tests Site Health Tests.
* @return array{direct: array<string, array{label: string, test: string}>} Amended tests.
*/
function perflab_add_imagick_avif_transparency_supported_test( array $tests ): array {
$tests['direct']['imagick_avif_transparency_supported'] = array(
'label' => __( 'Imagick AVIF Transparency Support', 'performance-lab' ),
'test' => 'perflab_imagick_avif_transparency_supported_test',
);
return $tests;
}
add_filter( 'site_status_tests', 'perflab_add_imagick_avif_transparency_supported_test' );
4 changes: 4 additions & 0 deletions plugins/performance-lab/includes/site-health/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,7 @@
// Cache-Control headers site health check.
require_once __DIR__ . '/bfcache-compatibility-headers/helper.php';
require_once __DIR__ . '/bfcache-compatibility-headers/hooks.php';

// Imagick AVIF transparency support site health check.
require_once __DIR__ . '/imagick-avif-transparency-support/helper.php';
require_once __DIR__ . '/imagick-avif-transparency-support/hooks.php';
26 changes: 26 additions & 0 deletions plugins/webp-uploads/helper.php
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,11 @@ function webp_uploads_mime_type_supported( string $mime_type ): bool {
* @return string The image output format. One of 'webp' or 'avif'.
*/
function webp_uploads_get_image_output_format(): string {
if ( ! webp_uploads_imagick_avif_transparency_supported() && (bool) wp_cache_get( 'webp_uploads_image_has_transparency', 'webp-uploads' ) ) {
wp_cache_delete( 'webp_uploads_image_has_transparency', 'webp-uploads' );
return 'webp';
}

$image_format = get_option( 'perflab_modern_image_format' );
return webp_uploads_sanitize_image_format( $image_format );
}
Expand Down Expand Up @@ -512,3 +517,24 @@ function webp_uploads_get_attachment_file_mime_type( int $attachment_id, string
$mime_type = $filetype['type'] ?? get_post_mime_type( $attachment_id );
return is_string( $mime_type ) ? $mime_type : '';
}

/**
* Checks if Imagick has AVIF transparency support.
*
* @since n.e.x.t
*
* @return bool True if Imagick has AVIF transparency support, false otherwise.
*/
function webp_uploads_imagick_avif_transparency_supported(): bool {
if ( extension_loaded( 'imagick' ) && class_exists( 'Imagick' ) ) {
$imagick_version = Imagick::getVersion();
if ( (bool) preg_match( '/\d+(?:\.\d+)+(?:-\d+)?/', $imagick_version['versionString'], $matches ) ) {
$imagick_version = $matches[0];
} else {
$imagick_version = $imagick_version['versionString'];
}
return version_compare( $imagick_version, '7.0.25', '>=' );
}

return false;
}
50 changes: 50 additions & 0 deletions plugins/webp-uploads/hooks.php
Original file line number Diff line number Diff line change
Expand Up @@ -966,3 +966,53 @@ function webp_uploads_convert_palette_png_to_truecolor( $file ): array {
}
add_filter( 'wp_handle_upload_prefilter', 'webp_uploads_convert_palette_png_to_truecolor' );
add_filter( 'wp_handle_sideload_prefilter', 'webp_uploads_convert_palette_png_to_truecolor' );

/**
* Checks if an image has transparency when uploading AVIF images with Imagick.
*
* @since n.e.x.t
*
* @param array<string, mixed>|mixed $file The uploaded file data.
* @return array<string, mixed> The modified file data.
*/
function webp_uploads_check_image_transparency( $file ): array {
if ( 'avif' !== webp_uploads_get_image_output_format() || webp_uploads_imagick_avif_transparency_supported() ) {
return $file;
}

// Because plugins do bad things.
if ( ! is_array( $file ) ) {
$file = array();
}
if ( ! isset( $file['tmp_name'], $file['name'] ) ) {
return $file;
}
if ( isset( $file['type'] ) && is_string( $file['type'] ) ) {
if ( ! str_starts_with( strtolower( $file['type'] ), 'image/' ) ) {
return $file;
}
} elseif ( ! str_starts_with( strtolower( (string) wp_check_filetype_and_ext( $file['tmp_name'], $file['name'] )['type'] ), 'image/' ) ) {
return $file;
}

$editor = wp_get_image_editor( $file['tmp_name'] );

if ( is_wp_error( $editor ) || ! $editor instanceof WP_Image_Editor_Imagick ) {
return $file;
}

$reflection = new ReflectionClass( $editor );
$image_property = $reflection->getProperty( 'image' );
if ( PHP_VERSION_ID < 80100 ) {
$image_property->setAccessible( true );
}
$imagick = $image_property->getValue( $editor );
Comment on lines +1004 to +1009
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's too bad that the image property is protected. Note how in the Image Placeholder plugin it actually extends WP_Image_Editor_Imagick with a Dominant_Color_Image_Editor_Imagick which it then uses. This is problematic though since multiple plugins can't each register their own editor classes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did try to create an anonymous class and added a method to expose the image property, but the Reflection API seemed better.


if ( $imagick instanceof Imagick ) {
wp_cache_set( 'webp_uploads_image_has_transparency', (bool) $imagick->getImageAlphaChannel(), 'webp-uploads' );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this set here in the cache since this function runs first and then later webp_uploads_get_image_output_format() runs to then read the value from the cache? This seems perhaps brittle. Normally setting and getting a cache value would happen in the context of the same function, not across separate functions, right? It feels like there may not be guarantees that the cached value would be set when it is checked for.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it works but is definitely brittle. The other approach I can think of is using a global variable or a transient with a short expiration time to store the transparency status and hash of any uploaded file for the current request, and then using it in the webp_uploads_filter_image_editor_output_format to determine the output format. If you have any ideas for a temporary storage that can be used within the same request, I’d appreciate that.
@adamsilverstein if you also have a better idea to handle this case, I’d appreciate that as well.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If these are both part of the same request, perhaps you could apply a filter inside the webp_uploads_filter_image_editor_output_format function, then add a filter here to set the value?

}

return $file;
}
add_filter( 'wp_handle_upload_prefilter', 'webp_uploads_check_image_transparency' );
add_filter( 'wp_handle_sideload_prefilter', 'webp_uploads_check_image_transparency' );