Prevent transparency loss in AVIF by falling back to WebP on older ImageMagick#2245
Prevent transparency loss in AVIF by falling back to WebP on older ImageMagick#2245b1ink0 wants to merge 44 commits intoWordPress:trunkfrom
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## trunk #2245 +/- ##
========================================
Coverage 69.17% 69.18%
========================================
Files 90 94 +4
Lines 7708 7856 +148
========================================
+ Hits 5332 5435 +103
- Misses 2376 2421 +45
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
plugins/webp-uploads/hooks.php
Outdated
| $imagick = $image_property->getValue( $editor ); | ||
|
|
||
| if ( $imagick instanceof Imagick ) { | ||
| wp_cache_set( 'webp_uploads_image_has_transparency', (bool) $imagick->getImageAlphaChannel(), 'webp-uploads' ); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
There are multiple approaches. I came up with this to solve it:
The most elegant way is to replace the WP_Image_Editor_Imagick class with a custom class WebP_Uploads_Image_Editor_Imagick, which can be used to detect transparency and provide access to the protected variable.
But this will interfere with other plugins if they are trying to use their own custom class. I was thinking of just dynamically extending any custom classes extended from the WP_Image_Editor_Imagick class.
There was a problem hiding this comment.
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?
The issue I faced was with the subsize-image generation. The webp_uploads_filter_image_editor_output_format filter is called with an empty filename, so with this approach the sub-size images were getting generated as AVIF.
Because of that, the only way to check the current processing image calling the output-format filter is to access the $image of the image-editor instance that is processing it. But since the core does not expose the image-editor instance through globals or filters, there seems to be no way to determine the currently processed image. To solve this, the only idea I could come up with was using our own custom Image Editor class.
I have implemented the mentioned approach in this commit 2247b61.
cc: @adamsilverstein
plugins/webp-uploads/hooks.php
Outdated
| $reflection = new ReflectionClass( $editor ); | ||
| $image_property = $reflection->getProperty( 'image' ); | ||
| if ( PHP_VERSION_ID < 80100 ) { | ||
| $image_property->setAccessible( true ); | ||
| } | ||
| $imagick = $image_property->getValue( $editor ); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
I did try to create an anonymous class and added a method to expose the image property, but the Reflection API seemed better.
plugins/webp-uploads/helper.php
Outdated
| function webp_uploads_imagick_avif_transparency_supported(): bool { | ||
| if ( extension_loaded( 'imagick' ) && class_exists( 'Imagick' ) ) { | ||
| $imagick_version = Imagick::getVersion(); | ||
| if ( (bool) preg_match( '/((?:[0-9]+\\.?)+)/', $imagick_version['versionString'], $matches ) ) { |
There was a problem hiding this comment.
| if ( (bool) preg_match( '/((?:[0-9]+\\.?)+)/', $imagick_version['versionString'], $matches ) ) { | |
| if ( (bool) preg_match( '/^\d+(?:\.\d+)+$/', $imagick_version['versionString'], $matches ) ) { |
There was a problem hiding this comment.
Updated regex to /\d+(?:\.\d+)+(?:-\d+)?/ for handling version string like this:
ImageMagick 7.1.1-15 Q16 aarch64 98eceff6a:20230729 https://imagemagick.orgCo-authored-by: Weston Ruter <westonruter@google.com>
There was a problem hiding this comment.
Would it make more sense to move this Site Health test to the Modern Image Formats plugin?
There was a problem hiding this comment.
Just seeing this comment. I think it makes sense to move all Site Health tests for images to the plugin. See #1781
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the Unlinked AccountsThe following contributors have not linked their GitHub and WordPress.org accounts: @johnmagbag1995, @wes-davis. Contributors, please read how to link your accounts to ensure your work is properly credited in WordPress releases. If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
| * @since n.e.x.t | ||
| */ | ||
|
|
||
| // @codeCoverageIgnoreStart |
There was a problem hiding this comment.
I'm curious why this needs to be ignored for code coverage?
There was a problem hiding this comment.
As far as I know, this section gets flagged by Codecov as the constant is always defined in the test environment, and as there is no way to undefine or modify a const value in PHP, we have to ignore it.
There was a problem hiding this comment.
That's right. However, this could be made slightly more concise:
if ( ! defined( 'ABSPATH' ) ) {
exit; // @codeCoverageIgnore
}This could be done at once for all files in the repo, since they all have it now, for example:
performance/plugins/performance-lab/load.php
Lines 18 to 22 in b4f9898
plugins/webp-uploads/class-webp-uploads-image-editor-imagick.php
Outdated
Show resolved
Hide resolved
| * has been compiled against ImageMagick version 6.4.0 or newer. | ||
| */ | ||
| if ( is_callable( array( $this->image, 'getImageAlphaChannel' ) ) ) { | ||
| if ( ! $this->image->getImageAlphaChannel() ) { |
There was a problem hiding this comment.
Have you checked that this works correctly with PNG, WebP and AVIF uploads with transparencies?
| } | ||
|
|
||
| // Walk through the pixels and look transparent pixels. | ||
| $w = $this->image->getImageWidth(); |
There was a problem hiding this comment.
this strikes me as very heavy and deserving of caching at the very least to avoid repeating.
I would also be fine saying Imagick 6.4 is required for transparency support :)
plugins/webp-uploads/helper.php
Outdated
| $avif_support = webp_uploads_mime_type_supported( 'image/avif' ); | ||
|
|
||
| if ( $avif_support && webp_uploads_check_image_transparency( $filename ) ) { | ||
| $avif_support = false; |
There was a problem hiding this comment.
AVIF should still be supported in Imagick is new enough, right? this line sets it as false if the image has a transparency without regard to the Imagick version.
misread that call ^^
|
@b1ink0 - overall this looks very good! I wonder if this belongs in core directly: does this impact AVIF with transparency uploads in core (without the upload transform from PNG->AVIF)? If so this probably belongs in core directly since we aim to support AVIF as completely as possible. You probably noticed the existing PNG transparency handling in core, we could add similar handling for AVIFs. |
|
@adamsilverstein I was just thinking all this complex logic is just so that if an image does not have transparency and the site has an AVIF transparency unsupported version of ImageMagick, we allow the AVIF conversion. The simplest way could be to turn off AVIF conversion completely, even for non-transparent images. |
Thats fine for the plugin! Happy to see this fix land here. I'm just thinking we may also be able to introduce a more general fix in core to handle this case. I will check to see if we have an existing ticket for this. |
…-transparency-loss-add-tests try: add avif transparency loss tests
|
I have been trying to write tests to improve code coverage, but it seems that I was able to run the tests correctly by modifying the Modified command: -"test-php:webp-uploads": "wp-env run tests-cli --env-cwd=/var/www/html/wp-content/plugins/performance composer test:webp-uploads",
+"test-php:webp-uploads": "wp-env run tests-wordpress --env-cwd=/var/www/html/wp-content/plugins/performance composer test:webp-uploads",So my question is: was there any reason to use Should we at least use the |
|
@b1ink0 That's a good question. It would seem to make sense that the tests should be run using |
|
Now with Next issue: we can’t really convert to the AVIF format with Imagick even in the no encode delegate for this image format `AVIF' @ error/constitute.c/WriteImage/1412The I did some digging and found that this is an upstream WordPress Docker image issue. In the Docker template file, But in the next step, the Dockerfile removes the recommended libraries here: Now, That depends on Which depends on
Here, Now we have a few options:
For now, I am removing the AVIF conversion–related tests, as this PR has been pending for a long time. |
|
Another review from Gemini: Based on the diff analysis of the Review SummaryThe changes address an issue where older versions of ImageMagick lose transparency when converting images to AVIF. The solution involves:
The implementation is robust, including a dynamic class alias to handle potential subclassing of Detailed Comments
|
|
Ohh now old tests are failing due to use of the |
|
I created an issue for the AVIF encoding issue here yesterday: docker-library/wordpress#996. It looks like they have already started working on it docker-library/wordpress#997. Hopefully, it gets merged soon. cc: @westonruter |
|
The tests are now passing for PHP 8.1+ and PHP 7.2+ versions due to the upstream fix, but PHP 8.1 and PHP 7.2 will not be fixed because support for this Docker image has been dropped (see docker-library/wordpress#992. We have the option to skip these failing tests for this specific version, or we can remove PHP 8.1 from testing. For now, I will skip these failing tests on these version. |
plugins/webp-uploads/class-webp-uploads-image-editor-imagick.php
Outdated
Show resolved
Hide resolved
plugins/webp-uploads/class-webp-uploads-image-editor-imagick.php
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'] ) ) { |
There was a problem hiding this comment.
If these aren't set, then should it not fall back to the fallback below as well? When would these not be set?
There was a problem hiding this comment.
Normally, these won’t fail and will most likely throw an exception if something fails in rare cases.
There was a problem hiding this comment.
While adding the fallback, I ran PHPStan, and it seems the PHPStan rules have been updated, which are now flagging these. As of now, these functions should exist in ImageMagick version 6.4.0 or newer, as they were added in 2008 (around 18 years ago). In that case, should we keep these checks?
------ --------------------------------------------------------------------------------------------------------
Line plugins/webp-uploads/class-webp-uploads-image-editor-imagick.php
------ --------------------------------------------------------------------------------------------------------
:102 Call to function method_exists() with Imagick and 'getImageAlphaChannel' will always evaluate to true.
:110 Call to function method_exists() with Imagick and 'getImageChannelMean' will always evaluate to true.
:110 Call to function method_exists() with Imagick and 'getImageChannelRange' will always evaluate to true.
------ --------------------------------------------------------------------------------------------------------
There was a problem hiding this comment.
Ah, yes. In that case, and since the try/catch will handle the case where they don't exist, then that seems good to remove the checks.
plugins/webp-uploads/class-webp-uploads-image-editor-imagick.php
Outdated
Show resolved
Hide resolved
plugins/webp-uploads/class-webp-uploads-image-editor-imagick.php
Outdated
Show resolved
Hide resolved
Co-authored-by: Weston Ruter <westonruter@google.com>
Co-authored-by: Weston Ruter <westonruter@google.com>
Co-authored-by: Weston Ruter <westonruter@google.com>
Co-authored-by: Weston Ruter <westonruter@google.com>
Co-authored-by: Weston Ruter <westonruter@google.com>
|
Response to Gemini feedback @westonruter please forward this to Gemini:
The
When the filename is null, it is most likely during sub-size generation. In that case, we use the current instance to get the file name. Since it’s a sub-size, the file name will be the same as the original image file name, which has already been processed. For sub-sizes, we don’t create a new editor instance because the static property has already cached it, so we just return the result from it.
I would like to highlight that the Regarding the hacky part, this was the cleanest way to ensure our custom class is loaded. Alternatively, we could call the filter directly here or call the hooked function directly.
The |
Summary
Fixes #2237
Relevant technical choices
Prevents transparent backgrounds from being lost when converting PNG images to AVIF format on older ImageMagick
TODO: