-
Notifications
You must be signed in to change notification settings - Fork 127
Fixes palette-based PNG uploads failing original full-size AVIF/WebP conversion under GD #2024
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?
Fixes palette-based PNG uploads failing original full-size AVIF/WebP conversion under GD #2024
Conversation
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## trunk #2024 +/- ##
==========================================
- Coverage 68.06% 67.77% -0.30%
==========================================
Files 92 93 +1
Lines 7626 7698 +72
==========================================
+ Hits 5191 5217 +26
- Misses 2435 2481 +46
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:
|
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: @IlyaZha, @mytory, @chimok. 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@b1ink0 Thank you, this looks great to me so far, just one comment.
It would be great to get @adamsilverstein's review on this as well.
|
||
return $file; | ||
} | ||
add_filter( 'wp_handle_upload_prefilter', 'webp_uploads_convert_palette_png_to_truecolor' ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're already using this in the test, but wouldn't it make sense to also add the same function as a hook callback to wp_handle_sideload_prefilter
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it makes sense. At first, I thought wp_handle_sideload
and media_handle_sideload
, which trigger the wp_handle_sideload_prefilter
, were not used in core, but after some digging, I found that core does trigger this filter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in 8ad675c
plugins/webp-uploads/hooks.php
Outdated
return $file; | ||
} | ||
|
||
if ( 'png' !== strtolower( pathinfo( $file['name'], PATHINFO_EXTENSION ) ) ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this look at the attachment metadata instead to see if the MIME type is image/png
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, we can just check if $file['type']
is image/png
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in 7443e68.
Co-authored-by: Weston Ruter <[email protected]>
Hi @b1ink0 and thanks for the PR.
Good catch! I understand the technical requirement for GD here and agree we need to address them to resolve this issue. My only concern is that by converting a palette image (up to 8 bit) to truecolor (eg. 24 bit), we might inadvertently bloat the image size. We tried to address this for Imagick in https://core.trac.wordpress.org/ticket/36477. I'd love to see some before/after size numbers for indexed PNG uploads converted to WebP and AVIF to confirm that isn't the case. I suspect we are ok given the lossy compression of WebP/AVIF vs the lossless PNG compression, but it would be worth checking and perhaps adding a unit test for this aspect. |
/** | ||
* Converts palette PNG images to true color PNG images. | ||
* | ||
* GD cannot convert palette-based PNG to WebP/AVIF formats, causing conversion failures. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if this fix belongs in core itself sice we inherently support this type of conversion with a simple filter. If we don't fix automatically, at the very least, we should inform the user why the upload failed and how to fix it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I located the Trac ticket: #59339 "Conversion to webp causes fatal error when original image is a palette image (as opposed to truecolor)"
Should we post an update there and start the discussion on where to go next?
|
||
// Check if required GD functions exist. | ||
if ( | ||
! function_exists( 'imagecreatefrompng' ) || |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 to defensive programming; however all of these functions have been around since PHP4/5 as as far as I know should be available to all WordPress installs with GD. So maybe we can remove this check?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would address the code coverage issue as well, if it is impossible to create a scenario where any of these functions don't exist.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, but actually will these functions be available if GD is not installed? If PHP is not compiled with --with-gd
then I don't think they will.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
plugins/webp-uploads/hooks.php
Outdated
} | ||
|
||
$image = imagecreatefrompng( $file['tmp_name'] ); | ||
if ( false === $image || imageistruecolor( $image ) ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm confused by this check. If $image is false, the conversion failed. In that case, don't we want to continue to the processing below?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When imagecreatefrompng()
returns false, it means the creation of the PNG failed. In that case, we can't convert it to truecolor because we need a valid GDImage resource for imagepalettetotruecolor()
to work properly. Returning early in this scenario is the best choice since there's no valid image resource to operate on.
I also combined two different checks in that condition, which I realize makes the logic less clear. Separate those out in 7f757c7 to improve readability.
} | ||
imagedestroy( $image ); | ||
|
||
return $file; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we set $file['name'] to the new file name before returning $file?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you're asking whether the conversion has changed the name of the file, it has not - we simply overwrote the same file with the converted version.
However, if you're suggesting that users should see a different filename when an image is converted to truecolor, we could add a suffix like -truecolor
to the filename. But I'm not sure whether we should do that, since this conversion is meant to be a transparent internal optimization rather than a user-visible change.
$modern_image_format_path = get_attached_file( $attachment_id ); | ||
$this->assertNotFalse( $modern_image_format_path ); | ||
$this->assertFileExists( $modern_image_format_path ); | ||
$this->assertGreaterThan( 0, (int) filesize( $modern_image_format_path ) ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we also add an assertion that the new image filesize isn't larger than the original image?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in 56e8dac.
'image_path' => $non_palette_png, | ||
'expected_changed' => false, | ||
'set_up' => function (): void { | ||
add_filter( 'wp_image_editors', array( $this, 'use_gd_image_editor' ) ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can the test just add this filter always, without needing a "set_up" attribute?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in e646941.
'set_up' => function (): void { | ||
add_filter( 'wp_image_editors', array( $this, 'use_gd_image_editor' ) ); | ||
}, | ||
'tear_down' => function (): void { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tear_down should not be required as all hooks should be reset between test runs as far as I know. Can you try removing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, we can remove this. I had added them because I was also testing using the ImageMagick editor, and since filters don't get removed between iterations of the data provider, I had to add setup and teardown.
But I removed the test for ImageMagick as I noticed ImageMagick wasn't working correctly when I ran the tests. When I tried to log Imagick::queryFormats()
, it returned an empty array. I'm not sure why - I tried running php -r "print_r(Imagick::queryFormats());"
in the wp-env
created tests-wordpress
container and it gave an array with all supported formats, but when I ran the same command in tests-cli
it gave an empty array. I'm not sure - do tests run in the tests-cli
container and not the tests-wordpress
container?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in e646941.
Co-authored-by: Adam Silverstein <[email protected]>
I ran some tests on a few indexed PNGs. As expected, converting them to truecolor PNGs increased the file size up to 2.5x in some cases. However, when these converted truecolor PNGs were then encoded to AVIF or WebP, the resulting file sizes were consistently smaller than the original indexed PNGs encoded directly to AVIF/WebP. cc: @adamsilverstein |
Summary
Fixes #2018, #1561, #1864
Relevant technical choices
This change hooks into
wp_handle_upload_prefilter
to detect palette-based (indexed-color) PNGs and convert them in-place to truecolor while preserving transparency before any AVIF/WebP encoding takes place. It preserves all existing upload metadata and only affects PNGs handled by the GD editor.