Make WordPress Core

Opened 7 weeks ago

Last modified 7 weeks ago

#65977 new defect (bug)

Error in the GD editor when converting from WebP

Reported by: jmrcodes Owned by:
Priority: normal Milestone: Awaiting Review
Component: Media Version: 7.1
Severity: normal Keywords: has-patch has-unit-tests
Cc: Focuses:

Description

Due to the PHP8.4 ValueError for out of range GD quality units, lossless WebP sources leak IMG_WEBP_LOSSLESS (101) into non-WebP encoders in WP_Image_Editor_GD, causing fatal errors on AVIF and JPEG conversion on PHP 8.4+.

In wp-includes/class-wp-image-editor-gd.php, set_quality() (line 607, @since 6.7.0):

Wrong mime checked (line 617)
'image/webp' === $this->mime_type tests the source type. $this->output_mime_type is never consulted, so the quality value 101 applies to every output format.

Rejection silently discarded (line 621)
parent::set_quality( 101 ) returns WP_Error invalid_image_quality — return value ignored.

Direct assignment bypasses everything (line 627)
$this->quality = $quality; writes 101 past the parent clamp of (0-100) and past the wp_editor_set_quality filter.

This results in broken media items, broken subsizes, 0 byte AVIF files, and a single lossless WebP conversion will break a wp media regenerate run.

[27-Aug-2026 11:00:12 UTC] PHP Fatal error:  Uncaught ValueError: imageavif(): Argument #3 ($quality) must be between -1 and 100 in /Users/james/Sites/wordpress-test-site/app/public/wp-includes/class-wp-image-editor.php:577
Stack trace:
#0 /Users/james/Sites/wordpress-test-site/app/public/wp-includes/class-wp-image-editor.php(577): imageavif(Object(GdImage), '/Users/james/Si...', 101)
#1 /Users/james/Sites/wordpress-test-site/app/public/wp-includes/class-wp-image-editor-gd.php(685): WP_Image_Editor->make_image('/Users/james/Si...', 'imageavif', Array)
#2 /Users/james/Sites/wordpress-test-site/app/public/wp-includes/class-wp-image-editor-gd.php(567): WP_Image_Editor_GD->make_image('/Users/james/Si...', 'imageavif', Array)
#3 /Users/james/Sites/wordpress-test-site/app/public/wp-includes/class-wp-image-editor-gd.php(495): WP_Image_Editor_GD->_save(Object(GdImage), '/Users/james/Si...', 'image/avif')
#4 /Users/james/Sites/wordpress-test-site/app/public/wp-admin/includes/image.php(343): WP_Image_Editor_GD->save('/Users/james/Si...')
#5 /Users/james/Sites/wordpress-test-site/app/public/wp-admin/includes/image.php(588): wp_create_image_subsizes('/Users/james/Si...', 392)
#6 /Users/james/Sites/wordpress-test-site/app/public/wp-admin/includes/media.php(535): wp_generate_attachment_metadata(392, '/Users/james/Si...')
#7 phar:///usr/local/bin/wp/vendor/wp-cli/media-command/src/Media_Command.php(383): media_handle_sideload(Array, false, '', Array)
#8 [internal function]: Media_Command->import(Array, Array)
#9 phar:///usr/local/bin/wp/vendor/wp-cli/wp-cli/php/WP_CLI/Dispatcher/CommandFactory.php(100): call_user_func(Array, Array, Array)
#10 [internal function]: WP_CLI\Dispatcher\CommandFactory::{closure:WP_CLI\Dispatcher\CommandFactory::create_subcommand():97}(Array, Array)
#11 phar:///usr/local/bin/wp/vendor/wp-cli/wp-cli/php/WP_CLI/Dispatcher/Subcommand.php(491): call_user_func(Object(Closure), Array, Array)
#12 phar:///usr/local/bin/wp/vendor/wp-cli/wp-cli/php/WP_CLI/Runner.php(431): WP_CLI\Dispatcher\Subcommand->invoke(Array, Array, Array)
#13 phar:///usr/local/bin/wp/vendor/wp-cli/wp-cli/php/WP_CLI/Runner.php(454): WP_CLI\Runner->run_command(Array, Array)
#14 phar:///usr/local/bin/wp/vendor/wp-cli/wp-cli/php/WP_CLI/Runner.php(1269): WP_CLI\Runner->run_command_and_exit()
#15 phar:///usr/local/bin/wp/vendor/wp-cli/wp-cli/php/WP_CLI/Bootstrap/LaunchRunner.php(28): WP_CLI\Runner->start()
#16 phar:///usr/local/bin/wp/vendor/wp-cli/wp-cli/php/bootstrap.php(83): WP_CLI\Bootstrap\LaunchRunner->process(Object(WP_CLI\Bootstrap\BootstrapState))
#17 phar:///usr/local/bin/wp/vendor/wp-cli/wp-cli/php/wp-cli.php(32): WP_CLI\bootstrap()
#18 phar:///usr/local/bin/wp/php/boot-phar.php(20): include('phar:///usr/loc...')
#19 /usr/local/bin/wp(4): include('phar:///usr/loc...')
#20 {main} thrown in /Users/james/Sites/wordpress-test-site/app/public/wp-includes/class-wp-image-editor.php on line 577

Change History (4)

This ticket was mentioned in ​PR #13300 on ​WordPress/wordpress-develop by ​@irozum.


7 weeks ago
#1

  • Keywords has-patch has-unit-tests added; needs-patch removed

WP_Image_Editor_GD::set_quality() checks $this->mime_type (the source image's mime type) instead of the output mime type when deciding whether to apply the WebP-lossless quality sentinel (IMG_WEBP_LOSSLESS, 101). Because of that, converting a lossless WebP source to any other format (AVIF, JPEG, etc.) still gets the quality forced to 101 — a value that is intentionally outside the normal [1,100] range and only valid for GD's WebP lossless encoder. On PHP 8.4+, passing 101 to imageavif()/imagejpeg() throws a ValueError, producing a fatal error, 0-byte sub-sizes, and broken wp media regenerate runs; on older PHP it silently applies the wrong quality to the non-WebP output.

This fixes the mime check to use $this->output_mime_type when it's set (falling back to $this->mime_type, matching the same fallback the parent class already uses in WP_Image_Editor::set_quality()), so the lossless override is only applied when the image is actually being encoded as WebP. It also removes the redundant parent::set_quality( $quality, $dims ) call for the 101 sentinel, since that value is always rejected by the parent's [1,100] validation and its WP_Error return was being silently discarded — the direct $this->quality assignment right below it was already the only thing doing anything.

Added a PHPUnit test (Tests_Image_Editor_GD::test_set_quality_does_not_leak_webp_lossless_quality_into_other_formats) that loads a lossless WebP fixture, saves it as JPEG, and asserts the quality used is the normal JPEG default (82) rather than the leaked WebP-lossless sentinel (101).

## Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Sonnet 5
Used for: Root-cause analysis, implementation, tests, and PR description. Reviewed by Igor Rozum.

​@adamsilverstein commented on ​PR #13300:


7 weeks ago
#2

Thanks for the PR @irozum - looks good.

It might be nice to extend the test to use the image_editor_output_format filter to set png output for webp uploads, then verify the code does what you expected.

Also, I'm wondering if we can improve this a bit:

  1. When a user uploads a file, WordPress tries to match that file's characteristics for the output (sub sized) files. Lossless uploads should result in lossless output files (transparent uploads->transparent output, interlace uploads->interlaced output etc.)
  2. When a user uploads a lossless file but has output mapped to use a lossy only format (eg webp mapped to jpeg), we can assume they want lossy output and proceed with the default value
  3. When the user uploads a lossless file and has output mapped to another format that also supports lossless (eg. webp maps to avif), _we should use the lossless output option_ (in the case of AVIF I believe that means setting to 100 quality.

So in short: I believe you should only apply the change as you have it to output formats that don't support lossless. Since 101 is a sort of magic number meaning lossless for WebP, we probably need to transform it to 100 for AVIF output (and visa versa for AVIF->WebP output.

ps. I assume you are doing your testing in the media library and I'm curious how the new client-side media encoding handles this, can you give it a test by dropping images onto the editor (the same output filter should apply).

#3 follow-up: @adamsilverstein
7 weeks ago

Thanks for the bug report @jmrcodes!

Out of curiosity how are you seeign this error? are you using a plugin to transform uploads to a different format? Please add some steps to reproduce, this helps us make sure we are solving the right problem at the right layer.

#4 in reply to: ↑ 3 @jmrcodes
7 weeks ago

Hey Adam, I came across this by chance while trying to measure performance gains converting uploads to AVIF.

I upload WebP files by default, but installed the PerfLab Modern Image Formats plugin and set AVIF as the default image format. There were some lossless WebP files in the Media Library and running wp media regenerate triggered errors while trying to convert.

Claude Code ran the cli command in an environment without ImageMagick which is why the bug was triggered.

I've been able to reproduce the error with a clean install, an mu-plugin to filter wp_image_editors to WP_Image_Editor_GD, adding lossless WebP files to the Media Library, activating Modern Image Formats, and then running regenerate through the cli. You also can't upload a lossless WebP after that.

It's a pretty niche problem - GD only, lossless WebP source - but assumed worth reporting.

The PR by @irozum addresses the issue of the magic WebP 101 leaking into the quality field for other formats, but I agree with your comments about matching lossless WebP 101 to lossless 100 AVIF.

Replying to adamsilverstein:

Thanks for the bug report @jmrcodes!

Out of curiosity how are you seeign this error? are you using a plugin to transform uploads to a different format? Please add some steps to reproduce, this helps us make sure we are solving the right problem at the right layer.

Note: See TracTickets for help on using tickets.