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 55 commits into
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## trunk #2245 +/- ##
==========================================
+ Coverage 69.14% 70.20% +1.06%
==========================================
Files 90 93 +3
Lines 7723 7874 +151
==========================================
+ Hits 5340 5528 +188
+ Misses 2383 2346 -37
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
| $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
| $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.
| 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: @xtecneo, @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. |
| * 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 :)
| $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. |
|
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 |
|
@mindctrl Would you please test this? Here's a build for convenience: webp-uploads.zip |
|
These new test failure are really confusing this is what is output for the mean and alpha range for PHP 7.3, 7.4 and 8.0: .[27-Feb-2026 08:27:37 UTC] /var/www/html/wp-content/plugins/performance/plugins/webp-uploads/tests/data/images/transparent.png - RGB Mean: Array
(
[mean] => 16383.75
[standardDeviation] => 0
)
[27-Feb-2026 08:27:37 UTC] Array
(
[minima] => 0
[maxima] => 0
)And for PHP 8.0+ and 7.2: .[27-Feb-2026 08:44:35 UTC] /var/www/html/wp-content/plugins/performance/plugins/webp-uploads/tests/data/images/transparent.png - RGB Mean: Array
(
[mean] => 0
[standardDeviation] => 0
)
[27-Feb-2026 08:44:35 UTC] Array
(
[minima] => 0
[maxima] => 0
)It should be all zero for fully transparent image but somehow its not for some PHP version.
The issue was due to a difference in the ImageMagick version across these different PHP versions, and the value of |
@westonruter thanks for the build. It seems to work fine. On my end, the PHP 7.4 stack I was using to test this was updated to include a newer version of ImageMagick. In this case, it means I can't test the same scenario where:
I was able to confirm that if AVIF isn't supported at all, the logic that ties into Site Health and the Media settings page shows the new notices as expected. I'll see if I can replicate the previous stack with the older version of ImageMagick. |
|
There was previously no practical way to test the exact issue this PR is intended to solve. I had attempted several times to create a Docker image with a broken ImageMagick version that reproduces the issue, but was unsuccessful because most search results and articles pointed to an incorrect commit as the fix. I eventually found a reliable way to reproduce the issue with the help of Codex. BackgroundAlex Chan's write-up correctly identifies the fixed release boundary as 6.9.12-68, but the commit linked in that article is not the commit that fixed the issue: Article: https://alexwlchan.net/2023/check-for-transparency/ While the version information is accurate, the linked commit should not be considered authoritative.
Root Cause In Code of ImageMagickBroken behavior in older IM6In the broken ImageMagick 6 AVIF writer, transparent images are still encoded through the older YCbCr path:
That path writes Y, Cb, and Cr image planes, but does not preserve alpha correctly for transparent PNG input. The result is that transparent pixels become opaque in the output AVIF. What the fix changedThe fix adds a matte-aware branch for AVIF writing. For images with transparency, the code switches to an RGB/RGBA path instead of the older YCbCr 4:2:0 path, using libheif modes such as:
That is the meaningful code change: the writer starts emitting RGBA data for matte images, which preserves the alpha channel instead of discarding it.
Reproducing the IssueTo properly test this PR, I created a separate branch based on the latest commit in this PR: Branch: https://github.com/b1ink0/performance/tree/fix/imagemagick-old-version-avif-transparency-loss-test ( b1ink0@06a01e8) This branch adds support for overriding the base Docker image used by @wordpress/env through a small patch, making it possible to test the issue directly within wp-env. Build and Test Instructions# Clone the repo
git clone --depth 1 --single-branch --branch trunk https://github.com/WordPress/performance.git
# Fetch the branch
git fetch https://github.com/b1ink0/performance.git \
fix/imagemagick-old-version-avif-transparency-loss-test:test-avif-test
git switch test-avif-test
# Patch wp-env
npm install
npm run postinstall
npm run build
# Build and start wp-env
npm run wp-env:build-im6-broken
npm run wp-env:start:im6-brokenVerification Steps
Screen.Recording.2026-06-17.at.1.53.02.PM.mov
Screen.Recording.2026-06-17.at.1.53.38.PM.movTo observe the broken behavior directly, temporarily disable the protection added in this PR by applying the following patch: Note: please copy using the copy button provided by GitHub else applying patch won't work. cat <<'PATCH' | git apply
diff --git a/plugins/webp-uploads/helper.php b/plugins/webp-uploads/helper.php
index cdebb2f4..460884c6 100644
--- a/plugins/webp-uploads/helper.php
+++ b/plugins/webp-uploads/helper.php
@@ -528,6 +528,7 @@ function webp_uploads_get_attachment_file_mime_type( int $attachment_id, string
* @return bool True if Imagick has AVIF transparency support, false otherwise.
*/
function webp_uploads_imagick_avif_transparency_supported( ?string $version = null ): bool {
+ return true;
$supported = false;
$imagick_version = $version;
PATCHAfter applying the patch:
Screen.Recording.2026-06-17.at.1.57.46.PM.movFor more detailed instructions, see: https://github.com/b1ink0/performance/blob/fix/imagemagick-old-version-avif-transparency-loss-test/tools/wp-env/README.md I used Codex to help identify the exact commits where the issue was introduced and fixed, and to generate the Docker image patch required to test both the broken and fixed ImageMagick versions. |
| $tests['direct']['imagick_avif_transparency_supported'] = array( | ||
| 'label' => __( 'Imagick AVIF Transparency Support', 'webp-uploads' ), | ||
| 'test' => 'webp_uploads_imagick_avif_transparency_supported_test', | ||
| ); |
There was a problem hiding this comment.
Should this test only be added if the AVIF format is enabled?
Also, should the settings screen show a warning with the format dropdown to advise against AVIF if transparency is not supported?
There was a problem hiding this comment.
Relevant code:
performance/plugins/webp-uploads/settings.php
Lines 144 to 158 in 7c8a612
The default should be webp if AVIF is not fully supported. Alternatively, we could make it so that AVIF is considered not supported at all if transparency isn't supported. That would make it simpler and less prone to a having a bad experience. Relevant code:
performance/plugins/webp-uploads/helper.php
Lines 349 to 369 in b1608b2
Additionally, this Site Health test can be omitted if AVIF isn't supported in the first place.
There was a problem hiding this comment.
The default should be webp if AVIF is not fully supported. Alternatively, we could make it so that AVIF is considered not supported at all if transparency isn't supported. That would make it simpler and less prone to a having a bad experience.
Yes, I had previously mentioned the same concern in the PR.
@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.
I had previously mentioned the same concern in the PR but did not receive a response, so I continued working on the approach where only images with transparency are converted to WebP, while non-transparent images continue to be converted to AVIF.
That said, I do not think the current implementation is the best solution. It feels more like a collection of workarounds due to the complexity of the underlying issue we are trying to solve. This code could potentially introduce more bugs than the number of users who would actually encounter the transparency-loss issue.
As you suggested, a simpler approach might be to disable AVIF when transparency support is not available and display a warning below the setting explaining why AVIF cannot be selected and a site health test if needed. That would be much easier to understand and maintain.
Regarding the current implementation, we could remove the workaround code entirely and rely on Git history if we ever need to revisit it. However, since this PR already contains 54 commits, I think it would be better to close this PR and I will create a new one if we decide to pursue the simpler approach.
There was a problem hiding this comment.
Should this test only be added if the AVIF format is enabled?
Also, should the settings screen show a warning with the format dropdown to advise against AVIF if transparency is not supported?
Yes I think both are valid.
There was a problem hiding this comment.
OK, let's go with the simpler approach. We sniff out for whether there are any issues with AVIF at all, and if so, we don't allow AVIF and show a notice explaining why AVIF isn't available. The Site Health test can also surface the fact that AVIF isn't available. Although, this would be somewhat redundant with the warning on the settings screen.
Sorry for the long review cycle on this. Glad we're getting close!
|
Based on comment #2245 (comment) Closing this in favor of the simpler approach implemented in #2540. If we need to revisit the more targeted workaround later, we can refer back to this PR. |
Summary
Fixes #2237
Relevant technical choices
Prevents transparent backgrounds from being lost when converting PNG images to AVIF format on older ImageMagick
TODO: