-
Notifications
You must be signed in to change notification settings - Fork 146
Prevent transparency loss in AVIF by falling back to WebP on older ImageMagick #2245
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: trunk
Are you sure you want to change the base?
Changes from 38 commits
ae7f22d
a1aff66
be0817b
3fe6954
2247b61
1a58d1c
fdf65ec
50f2be1
f8af1e9
991d7c9
fece741
9020b24
435f5ff
4da1758
717e768
c033c44
b28885e
6004d18
d218d0f
41f94e1
28f2583
b421017
433ea1c
b00d2d0
28d6020
4cf8302
a28415f
99a2704
92f6528
23f528b
06f67f7
4e1ac64
fec8109
50e74ad
d8acc81
0e0ee09
23e12d5
1045006
789906c
8140089
2c50ed7
d01f2d6
315d6c3
3190c5b
f0beb09
346a61e
3f55c70
59d39dc
be178ec
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,156 @@ | ||
| <?php | ||
| /** | ||
| * WordPress Image Editor Class for Image Manipulation through Imagick | ||
| * for transparency detection | ||
| * | ||
| * @package webp-uploads | ||
| * | ||
| * @since n.e.x.t | ||
| */ | ||
|
|
||
| // @codeCoverageIgnoreStart | ||
| if ( ! defined( 'ABSPATH' ) ) { | ||
| exit; // Exit if accessed directly. | ||
| } | ||
| // @codeCoverageIgnoreEnd | ||
|
|
||
| if ( class_exists( 'WebP_Uploads_Image_Editor_Imagick_Base' ) ) { | ||
|
|
||
| /** | ||
| * WordPress Image Editor Class for Image Manipulation through Imagick | ||
| * for transparency detection. | ||
| * | ||
| * @since n.e.x.t | ||
| * | ||
| * @see WP_Image_Editor | ||
| */ | ||
| class WebP_Uploads_Image_Editor_Imagick extends WebP_Uploads_Image_Editor_Imagick_Base { | ||
| /** | ||
| * The current instance of the image editor. | ||
| * | ||
| * @since n.e.x.t | ||
| * | ||
| * @var WebP_Uploads_Image_Editor_Imagick|null $current_instance The current instance. | ||
| */ | ||
| public static $current_instance = null; | ||
|
|
||
| /** | ||
| * Stores already checked images for transparency. | ||
| * | ||
| * @since n.e.x.t | ||
| * | ||
| * @var array<string, bool> Associative array with file paths as keys and transparency detection results as values. | ||
| */ | ||
| private static $checked_images = array(); | ||
|
|
||
| /** | ||
| * Load the image and set the current instance. | ||
| * | ||
| * @since n.e.x.t | ||
| * | ||
| * @return WP_Error|true True on success, WP_Error on failure. | ||
| */ | ||
| public function load() { | ||
| // @phpstan-ignore-next-line -- Parent class is created via class_alias at runtime. | ||
| $result = parent::load(); | ||
| if ( ! is_wp_error( $result ) ) { | ||
| self::$current_instance = $this; | ||
| } | ||
| return $result; | ||
| } | ||
|
|
||
| /** | ||
| * Get the file path of the image. | ||
| * | ||
| * @since n.e.x.t | ||
| * | ||
| * @return string The file path of the image. | ||
| */ | ||
| public function get_file(): string { | ||
| if ( property_exists( $this, 'file' ) && is_string( $this->file ) ) { | ||
| return $this->file; | ||
| } | ||
| return ''; | ||
| } | ||
|
|
||
| /** | ||
| * Looks for transparent pixels in the image. | ||
| * If there are none, it returns false. | ||
| * | ||
| * @since n.e.x.t | ||
| * | ||
| * @return bool|WP_Error True or false based on whether there are transparent pixels, or an error on failure. | ||
| */ | ||
| public function has_transparency() { | ||
| if ( ! property_exists( $this, 'image' ) || ! $this->image instanceof Imagick ) { | ||
| return new WP_Error( 'image_editor_has_transparency_error_no_image', __( 'Transparency detection no image found.', 'webp-uploads' ) ); | ||
| } | ||
|
|
||
| $file_path = $this->get_file(); | ||
| if ( isset( self::$checked_images[ $file_path ] ) ) { | ||
b1ink0 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| return self::$checked_images[ $file_path ]; | ||
| } | ||
| $transparency = false; | ||
|
|
||
| try { | ||
| /* | ||
| * Check if the image has an alpha channel if false, then it can't have transparency so return early. | ||
| * | ||
| * Note that Imagick::getImageAlphaChannel() is only available if Imagick | ||
| * has been compiled against ImageMagick version 6.4.0 or newer. | ||
| */ | ||
| if ( is_callable( array( $this->image, 'getImageAlphaChannel' ) ) ) { | ||
b1ink0 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| if ( Imagick::ALPHACHANNEL_UNDEFINED === $this->image->getImageAlphaChannel() ) { | ||
| self::$checked_images[ $file_path ] = false; | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| // Use mean and range to determine if there is any transparency more efficiently. | ||
| if ( is_callable( array( $this->image, 'getImageChannelMean' ) ) && is_callable( array( $this->image, 'getImageChannelRange' ) ) ) { | ||
b1ink0 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| $rgb_mean = $this->image->getImageChannelMean( Imagick::CHANNEL_ALL ); | ||
| $alpha_range = $this->image->getImageChannelRange( Imagick::CHANNEL_ALPHA ); | ||
|
|
||
| if ( isset( $rgb_mean['mean'], $alpha_range['maxima'] ) ) { | ||
|
||
| $maxima = (int) $alpha_range['maxima']; | ||
| $mean = (int) $rgb_mean['mean']; | ||
|
|
||
| if ( 0 > $maxima || 0 > $mean ) { | ||
| // For invalid values assume no transparency. | ||
| $transparency = false; | ||
| } elseif ( 0 === $maxima && 0 === $mean ) { | ||
| // Alpha channel is all zeros AND no RGB content indicates fully transparent image. | ||
| $transparency = true; | ||
| } elseif ( 0 === $maxima && $mean > 0 ) { | ||
| // Alpha maxima of 0 with RGB content present indicates no real alpha channel exists (hence fully opaque). | ||
| $transparency = false; | ||
| } elseif ( 0 < $maxima && 0 < $mean ) { | ||
| // Non-zero alpha values with RGB content present indicates some transparency. | ||
| $transparency = true; | ||
| } | ||
| } | ||
| } else { | ||
| // Fallback to walk through the pixels and look for transparent pixels. | ||
| $w = $this->image->getImageWidth(); | ||
| $h = $this->image->getImageHeight(); | ||
| for ( $x = 0; $x < $w; $x++ ) { | ||
| for ( $y = 0; $y < $h; $y++ ) { | ||
| $pixel = $this->image->getImagePixelColor( $x, $y ); | ||
| $color = $pixel->getColor( 2 ); | ||
| if ( $color['a'] < 255 ) { | ||
| $transparency = true; | ||
| break 2; | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| self::$checked_images[ $file_path ] = $transparency; | ||
| return $transparency; | ||
| } catch ( Exception $e ) { | ||
| /* translators: %s is the error message */ | ||
| return new WP_Error( 'image_editor_has_transparency_error', sprintf( __( 'Transparency detection failed: %s', 'webp-uploads' ), $e->getMessage() ) ); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,12 +21,16 @@ | |
| * @since 2.0.0 Added support for AVIF. | ||
| * @since 2.2.0 Added support for PNG. | ||
| * | ||
| * @param string|null $filename Optional. The filename. Default null. | ||
| * @return array<string, array<string>> An array of valid mime types, where the key is the mime type and the value is the extension type. | ||
| */ | ||
| function webp_uploads_get_upload_image_mime_transforms(): array { | ||
|
|
||
| function webp_uploads_get_upload_image_mime_transforms( ?string $filename = null ): array { | ||
| // Check the selected output format. | ||
| $output_format = webp_uploads_mime_type_supported( 'image/avif' ) ? webp_uploads_get_image_output_format() : 'webp'; | ||
| $output_format = webp_uploads_get_image_output_format(); | ||
|
|
||
| if ( 'avif' === $output_format && ( ! webp_uploads_mime_type_supported( 'image/avif' ) || webp_uploads_check_image_transparency( $filename ) ) ) { | ||
| $output_format = 'webp'; | ||
| } | ||
|
|
||
| $default_transforms = array( | ||
| 'image/jpeg' => array( 'image/' . $output_format ), | ||
|
|
@@ -512,3 +516,110 @@ 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 | ||
| * | ||
| * @param string|null $version Optional Imagick version string. If not provided, the version will be retrieved from the Imagick class. | ||
| * @return bool True if Imagick has AVIF transparency support, false otherwise. | ||
| */ | ||
| function webp_uploads_imagick_avif_transparency_supported( ?string $version = null ): bool { | ||
| $supported = false; | ||
| $imagick_version = $version; | ||
|
|
||
| if ( null === $imagick_version && extension_loaded( 'imagick' ) && class_exists( 'Imagick' ) ) { | ||
| $imagick_version = Imagick::getVersion(); | ||
| $imagick_version = $imagick_version['versionString']; | ||
| } | ||
|
|
||
| if ( null !== $imagick_version && '' !== $imagick_version && (bool) preg_match( '/\d+(?:\.\d+)+(?:-\d+)?/', $imagick_version, $matches ) ) { | ||
| $imagick_version = $matches[0]; | ||
| } | ||
|
|
||
| if ( null === $imagick_version || '' === $imagick_version ) { | ||
| return false; | ||
| } | ||
|
|
||
| $supported = version_compare( $imagick_version, '7.0.25', '>=' ); | ||
|
|
||
| /** | ||
| * Filters whether Imagick has AVIF transparency support. | ||
| * | ||
| * @since n.e.x.t | ||
| * | ||
| * @param bool $supported Whether AVIF transparency is supported. | ||
| */ | ||
| return (bool) apply_filters( 'webp_uploads_imagick_avif_transparency_supported', $supported ); | ||
| } | ||
|
|
||
| /** | ||
| * Checks if an image has transparency when AVIF output is configured and AVIF transparency support is missing. | ||
| * | ||
| * @since n.e.x.t | ||
| * | ||
| * @param string|null $filename The uploaded file name. | ||
| * @return bool Whether the image has transparency. | ||
| */ | ||
| function webp_uploads_check_image_transparency( ?string $filename ): bool { | ||
| static $processed_images = array(); | ||
|
|
||
| if ( 'avif' !== webp_uploads_get_image_output_format() || webp_uploads_imagick_avif_transparency_supported() ) { | ||
| return false; | ||
| } | ||
|
|
||
| if ( ! class_exists( 'WebP_Uploads_Image_Editor_Imagick' ) ) { | ||
| // Calls filter `wp_image_editors` internally which makes sure `webp_uploads_set_image_editors` is called. | ||
| wp_image_editor_supports(); | ||
| } | ||
|
|
||
| if ( ! class_exists( 'WebP_Uploads_Image_Editor_Imagick' ) ) { | ||
| return false; | ||
| } | ||
|
|
||
| /* | ||
| * When WordPress generates subsizes (thumbnail, medium, large, etc.), the 'image_editor_output_format' | ||
| * filter is triggered without a filename parameter. In these cases, we need to retrieve the filename | ||
| * from the current editor instance that was used to load the original image. This allows us to perform | ||
| * the transparency check on the source file even when generating derivative sizes. | ||
| */ | ||
| if ( null === $filename ) { | ||
| if ( null === WebP_Uploads_Image_Editor_Imagick::$current_instance ) { | ||
| return false; | ||
| } | ||
| $file = WebP_Uploads_Image_Editor_Imagick::$current_instance->get_file(); | ||
| if ( '' === $file ) { | ||
b1ink0 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| return false; | ||
| } | ||
| $filename = $file; | ||
| } | ||
|
|
||
| if ( ! is_string( $filename ) || ! file_exists( $filename ) ) { | ||
| return false; | ||
| } | ||
|
|
||
| if ( isset( $processed_images[ $filename ] ) ) { | ||
| return $processed_images[ $filename ]; | ||
| } | ||
| $processed_images[ $filename ] = false; | ||
|
|
||
| $editor = wp_get_image_editor( | ||
| $filename, | ||
| array( | ||
| 'methods' => array( | ||
| 'get_file', | ||
| 'has_transparency', | ||
| ), | ||
| ) | ||
| ); | ||
|
|
||
| if ( is_wp_error( $editor ) || ! $editor instanceof WebP_Uploads_Image_Editor_Imagick ) { | ||
| return false; | ||
| } | ||
|
|
||
| $has_transparency = $editor->has_transparency(); | ||
| $processed_images[ $filename ] = is_wp_error( $has_transparency ) ? false : $has_transparency; | ||
|
|
||
| return $processed_images[ $filename ]; | ||
| } | ||
|
Comment on lines
+520
to
+625
|
||
Uh oh!
There was an error while loading. Please reload this page.