Skip to content

Commit

Permalink
Media: Automatically convert HEIC images to JPEG
Browse files Browse the repository at this point in the history
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.


git-svn-id: https://develop.svn.wordpress.org/trunk@58849 602fd350-edb4-49c9-b593-d223f7449a82
  • Loading branch information
noisysocks committed Aug 5, 2024
1 parent 0a12ad2 commit 471a619
Show file tree
Hide file tree
Showing 13 changed files with 157 additions and 35 deletions.
2 changes: 1 addition & 1 deletion src/js/media/controllers/library.js
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ Library = wp.media.controller.State.extend(/** @lends wp.media.controller.Librar
isImageAttachment: function( attachment ) {
// If uploading, we know the filename but not the mime type.
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';
Expand Down
3 changes: 2 additions & 1 deletion src/wp-admin/includes/image.php
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,7 @@ function wp_copy_parent_attachment_properties( $cropped, $parent_attachment_id,
*
* @since 2.1.0
* @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 string $file Filepath of the attached image.
Expand All @@ -555,7 +556,7 @@ function wp_generate_attachment_metadata( $attachment_id, $file ) {
$support = false;
$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.
$metadata = wp_create_image_subsizes( $file, $attachment_id );
} elseif ( wp_attachment_is( 'video', $attachment ) ) {
Expand Down
5 changes: 2 additions & 3 deletions src/wp-includes/class-wp-image-editor-imagick.php
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,6 @@ public function set_quality( $quality = null ) {
$this->image->setImageCompressionQuality( $quality );
}
break;
case 'image/avif':
default:
$this->image->setImageCompressionQuality( $quality );
}
Expand Down Expand Up @@ -258,10 +257,10 @@ protected function update_size( $width = null, $height = null ) {
}

/*
* 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.
*/
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 );
$width = $size[0];
$height = $size[1];
Expand Down
22 changes: 1 addition & 21 deletions src/wp-includes/class-wp-image-editor.php
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,6 @@ protected function get_default_quality( $mime_type ) {
$quality = 86;
break;
case 'image/jpeg':
case 'image/avif':
default:
$quality = $this->default_quality;
}
Expand Down Expand Up @@ -366,26 +365,7 @@ protected function get_output_format( $filename = null, $mime_type = null ) {
$new_ext = $file_ext;
}

/**
* 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 );
$output_format = wp_get_image_editor_output_format( $filename, $mime_type );

if ( isset( $output_format[ $mime_type ] )
&& $this->supports_mime_type( $output_format[ $mime_type ] )
Expand Down
5 changes: 5 additions & 0 deletions src/wp-includes/compat.php
Original file line number Diff line number Diff line change
Expand Up @@ -549,3 +549,8 @@ function str_ends_with( $haystack, $needle ) {
if ( ! defined( 'IMG_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 );
}
14 changes: 12 additions & 2 deletions src/wp-includes/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
if ( $is_image ) {
/** This filter is documented in wp-includes/class-wp-image-editor.php */
$output_formats = apply_filters( 'image_editor_output_format', array(), $_dir . $filename, $mime_type );
$output_formats = wp_get_image_editor_output_format( $_dir . $filename, $mime_type );
$alt_types = array();

if ( ! empty( $output_formats[ $mime_type ] ) ) {
Expand Down Expand Up @@ -3120,6 +3119,7 @@ function wp_check_filetype_and_ext( $file, $filename, $mimes = null ) {
'image/tiff' => 'tif',
'image/webp' => 'webp',
'image/avif' => 'avif',
'image/heic' => 'heic',
)
);

Expand Down Expand Up @@ -3299,6 +3299,7 @@ function wp_check_filetype_and_ext( $file, $filename, $mimes = null ) {
* @since 4.7.1
* @since 5.8.0 Added support for WebP 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.
* @return string|false The actual mime type or false if the type cannot be determined.
Expand Down Expand Up @@ -3372,6 +3373,15 @@ function wp_get_image_mime( $file ) {
) {
$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 ) {
$mime = false;
}
Expand Down
72 changes: 69 additions & 3 deletions src/wp-includes/media.php
Original file line number Diff line number Diff line change
Expand Up @@ -4064,8 +4064,7 @@ function wp_get_image_editor( $path, $args = array() ) {

// Check and set the output mime type mapped to the input type.
if ( isset( $args['mime_type'] ) ) {
/** This filter is documented in wp-includes/class-wp-image-editor.php */
$output_format = apply_filters( 'image_editor_output_format', array(), $path, $args['mime_type'] );
$output_format = wp_get_image_editor_output_format( $path, $args['mime_type'] );
if ( isset( $output_format[ $args['mime_type'] ] ) ) {
$args['output_mime_type'] = $output_format[ $args['mime_type'] ];
}
Expand Down Expand Up @@ -4224,6 +4223,11 @@ function wp_plupload_default_settings() {
$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.
*
Expand Down Expand Up @@ -5483,12 +5487,17 @@ function _wp_add_additional_image_sizes() {
* Callback to enable showing of the user error when uploading .heic images.
*
* @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.
* @return array[] Modified settings for Plupload.js.
*/
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;
}

Expand Down Expand Up @@ -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.
return false;
}
Expand Down Expand Up @@ -6069,3 +6101,37 @@ function wp_high_priority_element_flag( $value = null ) {

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 );
}
2 changes: 1 addition & 1 deletion src/wp-includes/post.php
Original file line number Diff line number Diff line change
Expand Up @@ -6829,7 +6829,7 @@ function wp_attachment_is( $type, $post = null ) {

switch ( $type ) {
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 );

case 'audio':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -531,7 +531,7 @@ public function edit_media_item( $request ) {
);
}

$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 );
if ( ! in_array( $mime_type, $supported_types, true ) ) {
return new WP_Error(
Expand Down
Binary file added tests/phpunit/data/images/test-image.heic
Binary file not shown.
36 changes: 35 additions & 1 deletion tests/phpunit/tests/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -1355,6 +1355,11 @@ public function data_wp_get_image_mime() {
DIR_TESTDATA . '/images/avif-transparent.avif',
'image/avif',
),
// HEIC.
array(
DIR_TESTDATA . '/images/test-image.heic',
'image/heic',
),
);

return $data;
Expand Down Expand Up @@ -1384,7 +1389,7 @@ public function test_wp_getimagesize( $file, $expected ) {
}

/**
* Data profider for test_wp_getimagesize().
* Data provider for test_wp_getimagesize().
*/
public function data_wp_getimagesize() {
$data = array(
Expand Down Expand Up @@ -1541,6 +1546,35 @@ public function data_wp_getimagesize() {
return $data;
}

/**
* Tests that wp_getimagesize() correctly handles HEIC image files.
*
* @ticket 53645
*/
public function test_wp_getimagesize_heic() {
if ( ! is_callable( 'exif_imagetype' ) && ! function_exists( 'getimagesize' ) ) {
$this->markTestSkipped( 'The exif PHP extension is not loaded.' );
}

$file = DIR_TESTDATA . '/images/test-image.heic';

$editor = wp_get_image_editor( $file );
if ( is_wp_error( $editor ) || ! $editor->supports_mime_type( 'image/heic' ) ) {
$this->markTestSkipped( 'No HEIC support in the editor engine on this system.' );
}

$expected = array(
50,
50,
IMAGETYPE_HEIC,
'width="50" height="50"',
'mime' => 'image/heic',
);
$result = wp_getimagesize( $file );
$this->assertSame( $expected, $result );
}


/**
* @ticket 39550
* @dataProvider data_wp_check_filetype_and_ext
Expand Down
1 change: 1 addition & 0 deletions tests/phpunit/tests/image/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ public function data_file_is_displayable_image_negative() {
'test-image.jp2',
'test-image.psd',
'test-image-zip.tiff',
'test-image.heic',
);

return $this->text_array_to_dataprovider( $files );
Expand Down
28 changes: 27 additions & 1 deletion tests/phpunit/tests/image/resize.php
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,32 @@ public function test_resize_avif() {
$this->assertSame( IMAGETYPE_AVIF, $type );
}

/**
* Test resizing HEIC image.
*
* @ticket 53645
*/
public function test_resize_heic() {
$file = DIR_TESTDATA . '/images/test-image.heic';
$editor = wp_get_image_editor( $file );

// Check if the editor supports the HEIC mime type.
if ( is_wp_error( $editor ) || ! $editor->supports_mime_type( 'image/heic' ) ) {
$this->markTestSkipped( 'No HEIC support in the editor engine on this system.' );
}

$image = $this->resize_helper( $file, 25, 25 );

list( $w, $h, $type ) = wp_getimagesize( $image );

unlink( $image );

$this->assertSame( 'test-image-25x25.jpg', wp_basename( $image ) );
$this->assertSame( 25, $w );
$this->assertSame( 25, $h );
$this->assertSame( IMAGETYPE_JPEG, $type );
}

public function test_resize_larger() {
// image_resize() should refuse to make an image larger.
$image = $this->resize_helper( DIR_TESTDATA . '/images/test-image.jpg', 100, 100 );
Expand Down Expand Up @@ -235,6 +261,6 @@ protected function resize_helper( $file, $width, $height, $crop = false ) {
return $saved;
}

return $dest_file;
return $saved['path'];
}
}

0 comments on commit 471a619

Please sign in to comment.