Skip to content

#28077 - Use exif_imagetype() in file_is_displayable_image() - #14856

Open
skikken wants to merge 2 commits into
WordPress:trunkfrom
skikken:fix/28077-exif-imagetype-displayable
Open

skikken wants to merge 2 commits into
WordPress:trunkfrom
skikken:fix/28077-exif-imagetype-displayable

Conversation

@skikken

@skikken skikken commented Oct 25, 2026

Copy link
Copy Markdown

Summary

file_is_displayable_image() only needs the image type, but it obtains it from wp_getimagesize(), which parses the image header far enough to determine the width and height before returning the type at index 2. Reading the type from the file signature is sufficient for this check and avoids that parsing.

The function is a gate rather than a measurement: its only caller, wp_generate_attachment_metadata(), uses it to decide whether the attachment is a displayable image at all, and wp_create_image_subsizes() measures the image afterwards.

What this changes

exif_imagetype() reads the same header bytes as getimagesize() but stops after matching the signature, and it returns the same IMAGETYPE_* constant that getimagesize() returns at index 2. It is used when available:

if ( is_callable( 'exif_imagetype' ) ) {
	// Don't silence errors when in debug mode, unless running unit tests.
	if ( defined( 'WP_DEBUG' ) && WP_DEBUG && ! defined( 'WP_RUN_CORE_TESTS' ) ) {
		$image_type = exif_imagetype( $path );
	} else {
		$image_type = @exif_imagetype( $path );
	}
} else {
	$info       = wp_getimagesize( $path );
	$image_type = ! empty( $info ) ? $info[2] : false;
}

Three details:

The exif extension is optional, so the existing wp_getimagesize() behaviour is retained as a fallback, as comment #1 asks. The fallback goes through the wrapper rather than bare getimagesize(), so the WebP, AVIF and HEIC handling added to that wrapper still applies when exif is unavailable.

Error suppression is still needed. Comment #2 suggests exif_imagetype() only emits a notice, so the @ could be dropped. On current PHP it raises E_WARNING for a path that does not exist, and E_NOTICE for an empty file or for a directory, so dropping the suppression would introduce new warnings. The conditional pattern above is copied from wp_get_image_mime(), which already wraps exif_imagetype() this way.

The result stays a strict boolean, because the filter documents $result as a bool.

Behaviour change

The check is now based on the signature rather than on a successfully parsed header. A file whose signature is valid but whose remaining header data cannot be parsed is treated as displayable, where previously it was rejected here.

In practice the file still fails later: wp_create_image_subsizes() calls wp_get_image_editor(), which returns WP_Error: File is not an image. for such a file. Nothing is generated either way, but the rejection now happens downstream. A test covers this so the trade-off is explicit.

The two approaches were compared across all 56 image fixtures plus synthetic non-image, empty, nonexistent and truncated-file cases, and the results are identical apart from that one case. Type constants agree on every format, including AVIF, WebP, ICO, HEIC and TIFF.

Tests

tests/phpunit/tests/image/functions.php adds coverage for the result matching the previous answer on regular images and non-images, a strict boolean reaching the filter, a nonexistent path, the filter's return value being passed through, and a valid signature being accepted when the rest of the header cannot be parsed.

The signature test fails when the source change is reverted, which is the expected red state; the parity tests pass in both states by design, since they assert that existing behaviour is preserved.

Trac ticket: https://core.trac.wordpress.org/ticket/28077

Use of AI Tools

AI assistance: Yes
Tool(s): Claude
Model(s): Sonnet
Used for: test cases, code review


This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.

file_is_displayable_image() only needs the image type, but it called
wp_getimagesize(), which parses the image header to work out the dimensions
before returning the type. Reading the type from the file signature is enough
for this check, and it avoids the extra parsing for files that are only being
tested for displayability.

exif_imagetype() provides that signature check and reads the same header bytes.
It is used when the exif extension is available, with wp_getimagesize() kept as
a fallback so behaviour is unchanged when it is not. Errors are suppressed
unless WP_DEBUG is on and the tests are not running, following the pattern
already used by wp_get_image_mime().

Because the check is now based on the signature, a file whose signature is
valid is treated as displayable even if the rest of its header cannot be
parsed. Previously such a file was rejected here and discarded later by the
image editor.

Props joehoyle, jdgrimes, SergeyBiryukov.
Fixes #28077.
Cover the result matching the previous behaviour for regular images and
non-images, a strict boolean being passed to the filter, a nonexistent path,
the filter passing its value through, and a valid signature being accepted
when the rest of the header cannot be parsed.

Props joehoyle, jdgrimes, SergeyBiryukov.
Fixes #28077.
@github-actions

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs.

Core Committers: Use this line as a base for the props when committing in SVN:

Props whopiyush.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.


Props Bot commands

You can trigger Props Bot actions by commenting on this PR:

  • @propsbot add <username> to give props to a GitHub user. Requires write access.
  • @propsbot remove <username> to leave a user out of the props. Requires write access.
  • @propsbot refresh to refresh this list. Adding the props-bot label works too.

@github-actions

Copy link
Copy Markdown

⚠️ PHPStan Static Analysis

PHPStan reported 1 error when analyzing 4ac8f6a (workflow run). This comment is updated as new commits are pushed.

Errors

These are also shown as annotations on the changed files. They do not fail the check, but they should still be fixed before the pull request is committed. An error that is a false positive may instead warrant being added to a baseline.

Location Error Identifier First reported
src/wp-admin/includes/image.php:1186 Call to function is_callable() with 'exif_imagetype' will always evaluate to true. function.alreadyNarrowedType 4ac8f6a

Running PHPStan locally

To run the analysis on your own machine, use npm run typecheck:php, or composer phpstan outside the Docker environment.

@github-actions

Copy link
Copy Markdown

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant