Media: Automatically convert HEIC images to JPEG

Automatically create a JPEG version of uploaded HEIC images if the server has
a version of Imagick that supports HEIC. Conversion is done silently through
the existing `WP_Image_Editor` infrastructure that creates multiple sizes of
uploaded images.

This allows users to view HEIC images in WP Admin and use them in their posts
and pages regardless of whether their browser supports HEIC. Browser support
for HEIC is relatively low (only Safari) while the occurrence of HEIC images is
relatively common. The original HEIC image can be downloaded via a link on
the attachment page.

Props adamsilverstein, noisysocks, swissspidy, spacedmonkey, peterwilsoncc.
Fixes #53645.

Built from https://develop.svn.wordpress.org/trunk@58849


git-svn-id: http://core.svn.wordpress.org/trunk@58245 1a063a9b-81f0-0310-95a4-ce76da25c4cd
This commit is contained in:
noisysocks 2024-08-05 04:13:15 +00:00
parent 066d83f670
commit 3a703f86cd
11 changed files with 96 additions and 35 deletions

View File

@ -543,6 +543,7 @@ function wp_copy_parent_attachment_properties( $cropped, $parent_attachment_id,
* *
* @since 2.1.0 * @since 2.1.0
* @since 6.0.0 The `$filesize` value was added to the returned array. * @since 6.0.0 The `$filesize` value was added to the returned array.
* @since 6.7.0 The 'image/heic' mime type is supported.
* *
* @param int $attachment_id Attachment ID to process. * @param int $attachment_id Attachment ID to process.
* @param string $file Filepath of the attached image. * @param string $file Filepath of the attached image.
@ -555,7 +556,7 @@ function wp_generate_attachment_metadata( $attachment_id, $file ) {
$support = false; $support = false;
$mime_type = get_post_mime_type( $attachment ); $mime_type = get_post_mime_type( $attachment );
if ( preg_match( '!^image/!', $mime_type ) && file_is_displayable_image( $file ) ) { if ( 'image/heic' === $mime_type || ( preg_match( '!^image/!', $mime_type ) && file_is_displayable_image( $file ) ) ) {
// Make thumbnails and other intermediate sizes. // Make thumbnails and other intermediate sizes.
$metadata = wp_create_image_subsizes( $file, $attachment_id ); $metadata = wp_create_image_subsizes( $file, $attachment_id );
} elseif ( wp_attachment_is( 'video', $attachment ) ) { } elseif ( wp_attachment_is( 'video', $attachment ) ) {

View File

@ -219,7 +219,6 @@ class WP_Image_Editor_Imagick extends WP_Image_Editor {
$this->image->setImageCompressionQuality( $quality ); $this->image->setImageCompressionQuality( $quality );
} }
break; break;
case 'image/avif':
default: default:
$this->image->setImageCompressionQuality( $quality ); $this->image->setImageCompressionQuality( $quality );
} }
@ -258,10 +257,10 @@ class WP_Image_Editor_Imagick extends WP_Image_Editor {
} }
/* /*
* If we still don't have the image size, fall back to `wp_getimagesize`. This ensures AVIF images * If we still don't have the image size, fall back to `wp_getimagesize`. This ensures AVIF and HEIC images
* are properly sized without affecting previous `getImageGeometry` behavior. * are properly sized without affecting previous `getImageGeometry` behavior.
*/ */
if ( ( ! $width || ! $height ) && 'image/avif' === $this->mime_type ) { if ( ( ! $width || ! $height ) && ( 'image/avif' === $this->mime_type || 'image/heic' === $this->mime_type ) ) {
$size = wp_getimagesize( $this->file ); $size = wp_getimagesize( $this->file );
$width = $size[0]; $width = $size[0];
$height = $size[1]; $height = $size[1];

View File

@ -318,7 +318,6 @@ abstract class WP_Image_Editor {
$quality = 86; $quality = 86;
break; break;
case 'image/jpeg': case 'image/jpeg':
case 'image/avif':
default: default:
$quality = $this->default_quality; $quality = $this->default_quality;
} }
@ -366,26 +365,7 @@ abstract class WP_Image_Editor {
$new_ext = $file_ext; $new_ext = $file_ext;
} }
/** $output_format = wp_get_image_editor_output_format( $filename, $mime_type );
* Filters the image editor output format mapping.
*
* Enables filtering the mime type used to save images. By default,
* the mapping array is empty, so the mime type matches the source image.
*
* @see WP_Image_Editor::get_output_format()
*
* @since 5.8.0
*
* @param string[] $output_format {
* An array of mime type mappings. Maps a source mime type to a new
* destination mime type. Default empty array.
*
* @type string ...$0 The new mime type.
* }
* @param string $filename Path to the image.
* @param string $mime_type The source image mime type.
*/
$output_format = apply_filters( 'image_editor_output_format', array(), $filename, $mime_type );
if ( isset( $output_format[ $mime_type ] ) if ( isset( $output_format[ $mime_type ] )
&& $this->supports_mime_type( $output_format[ $mime_type ] ) && $this->supports_mime_type( $output_format[ $mime_type ] )

View File

@ -549,3 +549,8 @@ if ( ! defined( 'IMAGETYPE_AVIF' ) ) {
if ( ! defined( 'IMG_AVIF' ) ) { if ( ! defined( 'IMG_AVIF' ) ) {
define( 'IMG_AVIF', IMAGETYPE_AVIF ); define( 'IMG_AVIF', IMAGETYPE_AVIF );
} }
// IMAGETYPE_HEIC constant is not yet defined in PHP as of PHP 8.3.
if ( ! defined( 'IMAGETYPE_HEIC' ) ) {
define( 'IMAGETYPE_HEIC', 99 );
}

View File

@ -2706,8 +2706,7 @@ function wp_unique_filename( $dir, $filename, $unique_filename_callback = null )
* when regenerated. If yes, ensure the new file name will be unique and will produce unique sub-sizes. * when regenerated. If yes, ensure the new file name will be unique and will produce unique sub-sizes.
*/ */
if ( $is_image ) { if ( $is_image ) {
/** This filter is documented in wp-includes/class-wp-image-editor.php */ $output_formats = wp_get_image_editor_output_format( $_dir . $filename, $mime_type );
$output_formats = apply_filters( 'image_editor_output_format', array(), $_dir . $filename, $mime_type );
$alt_types = array(); $alt_types = array();
if ( ! empty( $output_formats[ $mime_type ] ) ) { if ( ! empty( $output_formats[ $mime_type ] ) ) {
@ -3120,6 +3119,7 @@ function wp_check_filetype_and_ext( $file, $filename, $mimes = null ) {
'image/tiff' => 'tif', 'image/tiff' => 'tif',
'image/webp' => 'webp', 'image/webp' => 'webp',
'image/avif' => 'avif', 'image/avif' => 'avif',
'image/heic' => 'heic',
) )
); );
@ -3299,6 +3299,7 @@ function wp_check_filetype_and_ext( $file, $filename, $mimes = null ) {
* @since 4.7.1 * @since 4.7.1
* @since 5.8.0 Added support for WebP images. * @since 5.8.0 Added support for WebP images.
* @since 6.5.0 Added support for AVIF images. * @since 6.5.0 Added support for AVIF images.
* @since 6.7.0 Added support for HEIC images.
* *
* @param string $file Full path to the file. * @param string $file Full path to the file.
* @return string|false The actual mime type or false if the type cannot be determined. * @return string|false The actual mime type or false if the type cannot be determined.
@ -3372,6 +3373,15 @@ function wp_get_image_mime( $file ) {
) { ) {
$mime = 'image/avif'; $mime = 'image/avif';
} }
if (
isset( $magic[1] ) &&
isset( $magic[2] ) &&
'ftyp' === hex2bin( $magic[1] ) &&
( 'heic' === hex2bin( $magic[2] ) || 'heif' === hex2bin( $magic[2] ) )
) {
$mime = 'image/heic';
}
} catch ( Exception $e ) { } catch ( Exception $e ) {
$mime = false; $mime = false;
} }

View File

@ -1449,7 +1449,7 @@ Library = wp.media.controller.State.extend(/** @lends wp.media.controller.Librar
isImageAttachment: function( attachment ) { isImageAttachment: function( attachment ) {
// If uploading, we know the filename but not the mime type. // If uploading, we know the filename but not the mime type.
if ( attachment.get('uploading') ) { if ( attachment.get('uploading') ) {
return /\.(jpe?g|png|gif|webp|avif)$/i.test( attachment.get('filename') ); return /\.(jpe?g|png|gif|webp|avif|heic)$/i.test( attachment.get('filename') );
} }
return attachment.get('type') === 'image'; return attachment.get('type') === 'image';

File diff suppressed because one or more lines are too long

View File

@ -4064,8 +4064,7 @@ function wp_get_image_editor( $path, $args = array() ) {
// Check and set the output mime type mapped to the input type. // Check and set the output mime type mapped to the input type.
if ( isset( $args['mime_type'] ) ) { if ( isset( $args['mime_type'] ) ) {
/** This filter is documented in wp-includes/class-wp-image-editor.php */ $output_format = wp_get_image_editor_output_format( $path, $args['mime_type'] );
$output_format = apply_filters( 'image_editor_output_format', array(), $path, $args['mime_type'] );
if ( isset( $output_format[ $args['mime_type'] ] ) ) { if ( isset( $output_format[ $args['mime_type'] ] ) ) {
$args['output_mime_type'] = $output_format[ $args['mime_type'] ]; $args['output_mime_type'] = $output_format[ $args['mime_type'] ];
} }
@ -4224,6 +4223,11 @@ function wp_plupload_default_settings() {
$defaults['avif_upload_error'] = true; $defaults['avif_upload_error'] = true;
} }
// Check if HEIC images can be edited.
if ( ! wp_image_editor_supports( array( 'mime_type' => 'image/heic' ) ) ) {
$defaults['heic_upload_error'] = true;
}
/** /**
* Filters the Plupload default settings. * Filters the Plupload default settings.
* *
@ -5483,12 +5487,17 @@ function _wp_add_additional_image_sizes() {
* Callback to enable showing of the user error when uploading .heic images. * Callback to enable showing of the user error when uploading .heic images.
* *
* @since 5.5.0 * @since 5.5.0
* @since 6.7.0 The default behavior is to enable heic uplooads as long as the server
* supports the format. The uploads are converted to JPEG's by default.
* *
* @param array[] $plupload_settings The settings for Plupload.js. * @param array[] $plupload_settings The settings for Plupload.js.
* @return array[] Modified settings for Plupload.js. * @return array[] Modified settings for Plupload.js.
*/ */
function wp_show_heic_upload_error( $plupload_settings ) { function wp_show_heic_upload_error( $plupload_settings ) {
$plupload_settings['heic_upload_error'] = true; // Check if HEIC images can be edited.
if ( ! wp_image_editor_supports( array( 'mime_type' => 'image/heic' ) ) ) {
$plupload_init['heic_upload_error'] = true;
}
return $plupload_settings; return $plupload_settings;
} }
@ -5586,6 +5595,29 @@ function wp_getimagesize( $filename, ?array &$image_info = null ) {
} }
} }
// For PHP versions that don't support HEIC images, extract the size info using Imagick when available.
if ( 'image/heic' === wp_get_image_mime( $filename ) ) {
$editor = wp_get_image_editor( $filename );
if ( is_wp_error( $editor ) ) {
return false;
}
// If the editor for HEICs is Imagick, use it to get the image size.
if ( $editor instanceof WP_Image_Editor_Imagick ) {
$size = $editor->get_size();
return array(
$size['width'],
$size['height'],
IMAGETYPE_HEIC,
sprintf(
'width="%d" height="%d"',
$size['width'],
$size['height']
),
'mime' => 'image/heic',
);
}
}
// The image could not be parsed. // The image could not be parsed.
return false; return false;
} }
@ -6069,3 +6101,37 @@ function wp_high_priority_element_flag( $value = null ) {
return $high_priority_element; return $high_priority_element;
} }
/**
* Determines the output format for the image editor.
*
* @since 6.7.0
* @access private
*
* @param string $filename Path to the image.
* @param string $mime_type The source image mime type.
* @return string[] An array of mime type mappings.
*/
function wp_get_image_editor_output_format( $filename, $mime_type ) {
/**
* Filters the image editor output format mapping.
*
* Enables filtering the mime type used to save images. By default,
* the mapping array is empty, so the mime type matches the source image.
*
* @see WP_Image_Editor::get_output_format()
*
* @since 5.8.0
* @since 6.7.0 The default was changed from array() to array( 'image/heic' => 'image/jpeg' ).
*
* @param string[] $output_format {
* An array of mime type mappings. Maps a source mime type to a new
* destination mime type. Default maps uploaded HEIC images to JPEG output.
*
* @type string ...$0 The new mime type.
* }
* @param string $filename Path to the image.
* @param string $mime_type The source image mime type.
*/
return apply_filters( 'image_editor_output_format', array( 'image/heic' => 'image/jpeg' ), $filename, $mime_type );
}

View File

@ -6829,7 +6829,7 @@ function wp_attachment_is( $type, $post = null ) {
switch ( $type ) { switch ( $type ) {
case 'image': case 'image':
$image_exts = array( 'jpg', 'jpeg', 'jpe', 'gif', 'png', 'webp', 'avif' ); $image_exts = array( 'jpg', 'jpeg', 'jpe', 'gif', 'png', 'webp', 'avif', 'heic' );
return in_array( $ext, $image_exts, true ); return in_array( $ext, $image_exts, true );
case 'audio': case 'audio':

View File

@ -531,7 +531,7 @@ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller {
); );
} }
$supported_types = array( 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif' ); $supported_types = array( 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif', 'image/heic' );
$mime_type = get_post_mime_type( $attachment_id ); $mime_type = get_post_mime_type( $attachment_id );
if ( ! in_array( $mime_type, $supported_types, true ) ) { if ( ! in_array( $mime_type, $supported_types, true ) ) {
return new WP_Error( return new WP_Error(

View File

@ -16,7 +16,7 @@
* *
* @global string $wp_version * @global string $wp_version
*/ */
$wp_version = '6.7-alpha-58848'; $wp_version = '6.7-alpha-58849';
/** /**
* Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema. * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.