[wp-trac] [WordPress Trac] #65188: Uploading PNG images with a larger number of colors causes timeouts and eventually CPU starvation
WordPress Trac
noreply at wordpress.org
Thu May 7 09:21:04 UTC 2026
#65188: Uploading PNG images with a larger number of colors causes timeouts and
eventually CPU starvation
---------------------------+-----------------------------
Reporter: romainmrhenry | Owner: (none)
Type: defect (bug) | Status: new
Priority: normal | Milestone: Awaiting Review
Component: Upload | Version: 6.8
Severity: normal | Keywords:
Focuses: performance |
---------------------------+-----------------------------
Since WordPress 6.8 there is new handling of PNG images.
Specifically in `thumbnail_image` at `wp-includes/class-wp-image-editor-
imagick.php:403`
Uploading a png image with a large number of colors, for example a
photographic image in .png format, causes the `getImageColors()` step at
line 506 to be extremely slow.
This seems to be the result of the chosen filter `FILTER_TRIANGLE`.
A standalone version of this function with some extra log lines:
{{{#!php
<?php
$image = new \Imagick( realpath( './foo.png' ) );
thumbnail_image( $image, 2048, 2048 );
function thumbnail_image( $image, $dst_w, $dst_h, $filter_name =
'FILTER_TRIANGLE' ) {
error_log( 'a' );
$current_colors = $image->getImageColors();
error_log( 'b' );
error_log( $current_colors );
$allowed_filters = array(
'FILTER_POINT',
'FILTER_BOX',
'FILTER_TRIANGLE',
'FILTER_HERMITE',
'FILTER_HANNING',
'FILTER_HAMMING',
'FILTER_BLACKMAN',
'FILTER_GAUSSIAN',
'FILTER_QUADRATIC',
'FILTER_CUBIC',
'FILTER_CATROM',
'FILTER_MITCHELL',
'FILTER_LANCZOS',
'FILTER_BESSEL',
'FILTER_SINC',
);
/**
* Set the filter value if '$filter_name' name is in the allowed
list and the related
* Imagick constant is defined or fall back to the default filter.
*/
if ( in_array( $filter_name, $allowed_filters, true ) && defined(
'Imagick::' . $filter_name ) ) {
$filter = constant( 'Imagick::' . $filter_name );
} else {
$filter = defined( 'Imagick::FILTER_TRIANGLE' ) ?
Imagick::FILTER_TRIANGLE : false;
}
try {
$size = null;
try {
error_log( '1' );
$size = $image->getImageGeometry();
error_log( '2' );
} catch ( Exception $e ) {
return new WP_Error( 'invalid_image', __( 'Could
not read image size.' ) );
}
/*
* To be more efficient, resample large images to
5x the destination size before resizing
* whenever the output size is less that 1/3 of the
original image size (1/3^2 ~= .111),
* unless we would be resampling to a scale smaller
than 128x128.
*/
if ( is_callable( array( $image, 'sampleImage' ) ) ) {
$resize_ratio = ( $dst_w / $size['width'] ) * (
$dst_h / $size['height'] );
$sample_factor = 5;
if ( $resize_ratio < .111 && ( $dst_w *
$sample_factor > 128 && $dst_h * $sample_factor > 128 ) ) {
error_log( '3' );
$image->sampleImage( $dst_w *
$sample_factor, $dst_h * $sample_factor );
error_log( '4' );
}
}
/*
* Use resizeImage() when it's available and a
valid filter value is set.
* Otherwise, fall back to the scaleImage() method
for resizing, which
* results in better image quality over
resizeImage() with default filter
* settings and retains backward compatibility with
pre 4.5 functionality.
*/
if ( is_callable( array( $image, 'resizeImage' ) ) &&
$filter ) {
error_log( '5' );
$image->setOption( 'filter:support', '2.0' );
$image->resizeImage( $dst_w, $dst_h, $filter, 1 );
error_log( '6' );
} else {
error_log( '7' );
$image->scaleImage( $dst_w, $dst_h );
error_log( '8' );
}
error_log( '9' );
$image->setOption( 'png:compression-filter', '5' );
$image->setOption( 'png:compression-level', '9' );
$image->setOption( 'png:compression-strategy', '1' );
error_log( '10' );
// Check to see if a PNG is indexed, and find the pixel
depth.
if ( is_callable( array( $image, 'getImageDepth' ) ) ) {
error_log( '11' );
$indexed_pixel_depth = $image->getImageDepth();
error_log( $indexed_pixel_depth );
error_log( '12' );
// Indexed PNG files get some additional handling.
if ( 0 < $indexed_pixel_depth && 8 >=
$indexed_pixel_depth ) {
// Check for an alpha channel.
error_log( '13' );
if (
is_callable( array( $image,
'getImageAlphaChannel' ) )
&& $image->getImageAlphaChannel()
) {
$image->setOption( 'png:include-
chunk', 'tRNS' );
} else {
$image->setOption( 'png:exclude-
chunk', 'all' );
}
error_log( '14' );
// Reduce colors in the images to maximum
needed, using the global colorspace.
$max_colors = pow( 2, $indexed_pixel_depth
);
error_log( $max_colors );
if ( is_callable( array( $image,
'getImageColors' ) ) ) {
$current_colors =
$image->getImageColors();
error_log( $current_colors );
$max_colors = min( $max_colors,
$current_colors );
}
error_log( $max_colors );
error_log( '15' );
$image->quantizeImage( $max_colors,
$image->getColorspace(), 0, false, false );
error_log( '16' );
error_log( 'c' );
$current_colors =
$image->getImageColors();
error_log( 'd' );
error_log( $current_colors );
/**
* If the colorspace is 'gray', use the
png8 format to ensure it stays indexed.
*/
if ( Imagick::COLORSPACE_GRAY ===
$image->getImageColorspace() ) {
$image->setOption( 'png:format',
'png8' );
}
}
}
/*
* If alpha channel is not defined, set it opaque.
*
* Note that Imagick::getImageAlphaChannel() is
only available if Imagick
* has been compiled against ImageMagick version
6.4.0 or newer.
*/
if ( is_callable( array( $image, 'getImageAlphaChannel' )
)
&& is_callable( array( $image,
'setImageAlphaChannel' ) )
&& defined( 'Imagick::ALPHACHANNEL_UNDEFINED' )
&& defined( 'Imagick::ALPHACHANNEL_OPAQUE' )
) {
if ( $image->getImageAlphaChannel() ===
Imagick::ALPHACHANNEL_UNDEFINED ) {
$image->setImageAlphaChannel(
Imagick::ALPHACHANNEL_OPAQUE );
}
}
// Limit the bit depth of resized images.
if ( is_callable( array( $image, 'getImageDepth' ) ) &&
is_callable( array( $image, 'setImageDepth' ) ) ) {
/**
* Filters the maximum bit depth of resized
images.
*
* This filter only applies when resizing using
the Imagick editor since GD
* does not support getting or setting bit depth.
*
* Use this to adjust the maximum bit depth of
resized images.
*
* @since 6.8.0
*
* @param int $max_depth The maximum bit depth.
Default is the input depth.
* @param int $image_depth The bit depth of the
original image.
*/
$max_depth = $image->getImageDepth();
$image->setImageDepth( $max_depth );
}
} catch ( Exception $e ) {
return new WP_Error( 'image_resize_error',
$e->getMessage() );
}
}
}}}
-------
It also seems that there is some confusion around channel depth and colors
in this function.
The `$max_colors` is a value per channel while `$current_colors` is for
all channels combined.
I think `$max_colors` should be multiplied by 3.
-----
It is extremely easy to DOS a WordPress server that allows file uploads by
feeding it photographic PNG images.
--
Ticket URL: <https://core.trac.wordpress.org/ticket/65188>
WordPress Trac <https://core.trac.wordpress.org/>
WordPress publishing platform
More information about the wp-trac
mailing list