Media/Upload: rotate images on upload according to EXIF Orientation.

Props msaggiorato, wpdavis, markoheijnen, dhuyvetter, msaggiorato, n7studios, triplejumper12, pbiron, mikeschroder, joemcgill, azaozz.

Fixes #14459.
Built from https://develop.svn.wordpress.org/trunk@46202


git-svn-id: http://core.svn.wordpress.org/trunk@46014 1a063a9b-81f0-0310-95a4-ce76da25c4cd
This commit is contained in:
Andrew Ozz 2019-09-20 18:21:57 +00:00
parent 86f7ac6669
commit 29ea4c6a4c
4 changed files with 187 additions and 16 deletions

View File

@ -158,6 +158,37 @@ function wp_update_image_subsizes( $attachment_id ) {
return _wp_make_subsizes( $missing_sizes, $image_file, $image_meta, $attachment_id );
}
/**
* Updates the attached file and image meta data when the original image was edited.
*
* @since 5.3.0
* @access private
*
* @param array $saved_data The data retirned from WP_Image_Editor after successfully saving an image.
* @param string $original_file Path to the original file.
* @param array $image_meta The image meta data.
* @param int $attachment_id The attachment post ID.
* @return array The updated image meta data.
*/
function _wp_image_meta_replace_original( $saved_data, $original_file, $image_meta, $attachment_id ) {
$new_file = $saved_data['path'];
// Update the attached file meta.
update_attached_file( $attachment_id, $new_file );
// Width and height of the new image.
$image_meta['width'] = $saved_data['width'];
$image_meta['height'] = $saved_data['height'];
// Make the file path relative to the upload dir.
$image_meta['file'] = _wp_relative_upload_path( $new_file );
// Store the original image file name in image_meta.
$image_meta['original_image'] = wp_basename( $original_file );
return $image_meta;
}
/**
* Creates image sub-sizes, adds the new data to the image meta `sizes` array, and updates the image metadata.
*
@ -222,30 +253,58 @@ function wp_create_image_subsizes( $file, $attachment_id ) {
// Resize the image
$resized = $editor->resize( $threshold, $threshold );
$rotated = null;
// If there is EXIF data, rotate according to EXIF Orientation.
if ( ! is_wp_error( $resized ) && is_array( $exif_meta ) ) {
$resized = $editor->maybe_exif_rotate();
$rotated = $resized;
}
if ( ! is_wp_error( $resized ) ) {
// TODO: EXIF rotate here.
// By default the editor will append `{width}x{height}` to the file name of the resized image.
// Better to append the threshold size instead so the image file name would be like "my-image-2560.jpg"
// and not look like a "regular" sub-size.
// Append the threshold size to the image file name. It will look like "my-image-2560.jpg".
// This doesn't affect the sub-sizes names as they are generated from the original image (for best quality).
$saved = $editor->save( $editor->generate_filename( $threshold ) );
if ( ! is_wp_error( $saved ) ) {
$new_file = $saved['path'];
$image_meta = _wp_image_meta_replace_original( $saved, $file, $image_meta, $attachment_id );
// Update the attached file meta.
update_attached_file( $attachment_id, $new_file );
// If the image was rotated update the stored EXIF data.
if ( true === $rotated && ! empty( $image_meta['image_meta']['orientation'] ) ) {
$image_meta['image_meta']['orientation'] = 1;
}
} else {
// TODO: handle errors.
}
} else {
// TODO: handle errors.
}
} elseif ( ! empty( $exif_meta['orientation'] ) && (int) $exif_meta['orientation'] !== 1 ) {
// Rotate the whole original image if there is EXIF data and "orientation" is not 1.
// Width and height of the new image.
$image_meta['width'] = $saved['width'];
$image_meta['height'] = $saved['height'];
$editor = wp_get_image_editor( $file );
// Make the file path relative to the upload dir.
$image_meta['file'] = _wp_relative_upload_path( $new_file );
if ( is_wp_error( $editor ) ) {
// This image cannot be edited.
return $image_meta;
}
// Store the original image file name in image_meta.
$image_meta['original_image'] = wp_basename( $file );
// Rotate the image
$rotated = $editor->maybe_exif_rotate();
if ( true === $rotated ) {
// Append `-rotated` to the image file name.
$saved = $editor->save( $editor->generate_filename( 'rotated' ) );
if ( ! is_wp_error( $saved ) ) {
$image_meta = _wp_image_meta_replace_original( $saved, $file, $image_meta, $attachment_id );
// Update the stored EXIF data.
if ( ! empty( $image_meta['image_meta']['orientation'] ) ) {
$image_meta['image_meta']['orientation'] = 1;
}
} else {
// TODO: handle errors.
}
}
}
@ -327,6 +386,15 @@ function _wp_make_subsizes( $new_sizes, $file, $image_meta, $attachment_id ) {
return $image_meta;
}
// If stored EXIF data exists, rotate the source image before creating sub-sizes.
if ( ! empty( $image_meta['image_meta'] ) ) {
$rotated = $editor->maybe_exif_rotate();
if ( is_wp_error( $rotated ) ) {
// TODO: handle errors.
}
}
if ( method_exists( $editor, 'make_subsize' ) ) {
foreach ( $new_sizes as $new_size_name => $new_size_data ) {
$new_size_meta = $editor->make_subsize( $new_size_data );

View File

@ -566,7 +566,7 @@ class WP_Image_Editor_Imagick extends WP_Image_Editor {
try {
$this->image->rotateImage( new ImagickPixel( 'none' ), 360 - $angle );
// Normalise Exif orientation data so that display is consistent across devices.
// Normalise EXIF orientation data so that display is consistent across devices.
if ( is_callable( array( $this->image, 'setImageOrientation' ) ) && defined( 'Imagick::ORIENTATION_TOPLEFT' ) ) {
$this->image->setImageOrientation( Imagick::ORIENTATION_TOPLEFT );
}
@ -602,12 +602,37 @@ class WP_Image_Editor_Imagick extends WP_Image_Editor {
if ( $vert ) {
$this->image->flopImage();
}
// Normalise EXIF orientation data so that display is consistent across devices.
if ( is_callable( array( $this->image, 'setImageOrientation' ) ) && defined( 'Imagick::ORIENTATION_TOPLEFT' ) ) {
$this->image->setImageOrientation( Imagick::ORIENTATION_TOPLEFT );
}
} catch ( Exception $e ) {
return new WP_Error( 'image_flip_error', $e->getMessage() );
}
return true;
}
/**
* Check if a JPEG image has EXIF Orientation tag and rotate it if needed.
*
* As ImageMagick copies the EXIF data to the flipped/rotated image, proceed only
* if EXIF Orientation can be reset afterwards.
*
* @since 5.3.0
*
* @return bool|WP_Error True if the image was rotated. False if no EXIF data or if the image doesn't need rotation.
* WP_Error if error while rotating.
*/
public function maybe_exif_rotate() {
if ( is_callable( array( $this->image, 'setImageOrientation' ) ) && defined( 'Imagick::ORIENTATION_TOPLEFT' ) ) {
return parent::maybe_exif_rotate();
} else {
return new WP_Error( 'write_exif_error', __( 'The image cannot be rotated because the embedded meta data cannot be updated.' ) );
}
}
/**
* Saves current image to file.
*

View File

@ -384,6 +384,84 @@ abstract class WP_Image_Editor {
return "{$this->size['width']}x{$this->size['height']}";
}
/**
* Check if a JPEG image has EXIF Orientation tag and rotate it if needed.
*
* @since 5.3.0
*
* @return bool|WP_Error True if the image was rotated. False if not rotated (no EXIF data or the image doesn't need to be rotated).
* WP_Error if error while rotating.
*/
public function maybe_exif_rotate() {
$orientation = null;
if ( is_callable( 'exif_read_data' ) && 'image/jpeg' === $this->mime_type ) {
$exif_data = @exif_read_data( $this->file );
if ( ! empty( $exif_data['Orientation'] ) ) {
$orientation = (int) $exif_data['Orientation'];
}
}
/**
* Filters the `$orientation` value to correct it before rotating or to prevemnt rotating the image.
*
* @since 5.3.0
*
* @param int $orientation EXIF Orientation value as retrieved from the image file.
* @param string $file Path to the image file.
*/
$orientation = apply_filters( 'wp_image_maybe_exif_rotate', $orientation, $this->file );
if ( ! $orientation || $orientation === 1 ) {
return false;
}
switch ( $orientation ) {
case 2:
// Flip horizontally.
$result = $this->flip( true, false );
break;
case 3:
// Rotate 180 degrees or flip horizontally and vertically.
// Flipping seems faster/uses less resources.
$result = $this->flip( true, true );
break;
case 4:
// Flip vertically.
$result = $this->flip( false, true );
break;
case 5:
// Rotate 90 degrees counter-clockwise and flip vertically.
$result = $this->rotate( 90 );
if ( ! is_wp_error( $result ) ) {
$result = $this->flip( false, true );
}
break;
case 6:
// Rotate 90 degrees clockwise (270 counter-clockwise).
$result = $this->rotate( 270 );
break;
case 7:
// Rotate 90 degrees counter-clockwise and flip horizontally.
$result = $this->rotate( 90 );
if ( ! is_wp_error( $result ) ) {
$result = $this->flip( true, false );
}
break;
case 8:
// Rotate 90 degrees counter-clockwise.
$result = $this->rotate( 90 );
break;
}
return $result;
}
/**
* Either calls editor's save function or handles file as a stream.
*

View File

@ -13,7 +13,7 @@
*
* @global string $wp_version
*/
$wp_version = '5.3-alpha-46201';
$wp_version = '5.3-alpha-46202';
/**
* Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.