Make WordPress Core

Changeset 46202


Ignore:
Timestamp:
09/20/2019 06:20:26 PM (5 years ago)
Author:
azaozz
Message:

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

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

Fixes #14459.

Location:
trunk/src
Files:
3 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-admin/includes/image.php

    r46076 r46202  
    160160
    161161/**
     162 * Updates the attached file and image meta data when the original image was edited.
     163 *
     164 * @since 5.3.0
     165 * @access private
     166 *
     167 * @param array  $saved_data    The data retirned from WP_Image_Editor after successfully saving an image.
     168 * @param string $original_file Path to the original file.
     169 * @param array  $image_meta    The image meta data.
     170 * @param int    $attachment_id The attachment post ID.
     171 * @return array The updated image meta data.
     172 */
     173function _wp_image_meta_replace_original( $saved_data, $original_file, $image_meta, $attachment_id ) {
     174    $new_file = $saved_data['path'];
     175
     176    // Update the attached file meta.
     177    update_attached_file( $attachment_id, $new_file );
     178
     179    // Width and height of the new image.
     180    $image_meta['width']  = $saved_data['width'];
     181    $image_meta['height'] = $saved_data['height'];
     182
     183    // Make the file path relative to the upload dir.
     184    $image_meta['file'] = _wp_relative_upload_path( $new_file );
     185
     186    // Store the original image file name in image_meta.
     187    $image_meta['original_image'] = wp_basename( $original_file );
     188
     189    return $image_meta;
     190}
     191
     192/**
    162193 * Creates image sub-sizes, adds the new data to the image meta `sizes` array, and updates the image metadata.
    163194 *
     
    223254        // Resize the image
    224255        $resized = $editor->resize( $threshold, $threshold );
     256        $rotated = null;
     257
     258        // If there is EXIF data, rotate according to EXIF Orientation.
     259        if ( ! is_wp_error( $resized ) && is_array( $exif_meta ) ) {
     260            $resized = $editor->maybe_exif_rotate();
     261            $rotated = $resized;
     262        }
    225263
    226264        if ( ! is_wp_error( $resized ) ) {
    227             // TODO: EXIF rotate here.
    228             // By default the editor will append `{width}x{height}` to the file name of the resized image.
    229             // Better to append the threshold size instead so the image file name would be like "my-image-2560.jpg"
    230             // and not look like a "regular" sub-size.
     265            // Append the threshold size to the image file name. It will look like "my-image-2560.jpg".
    231266            // This doesn't affect the sub-sizes names as they are generated from the original image (for best quality).
    232267            $saved = $editor->save( $editor->generate_filename( $threshold ) );
    233268
    234269            if ( ! is_wp_error( $saved ) ) {
    235                 $new_file = $saved['path'];
    236 
    237                 // Update the attached file meta.
    238                 update_attached_file( $attachment_id, $new_file );
    239 
    240                 // Width and height of the new image.
    241                 $image_meta['width']  = $saved['width'];
    242                 $image_meta['height'] = $saved['height'];
    243 
    244                 // Make the file path relative to the upload dir.
    245                 $image_meta['file'] = _wp_relative_upload_path( $new_file );
    246 
    247                 // Store the original image file name in image_meta.
    248                 $image_meta['original_image'] = wp_basename( $file );
     270                $image_meta = _wp_image_meta_replace_original( $saved, $file, $image_meta, $attachment_id );
     271
     272                // If the image was rotated update the stored EXIF data.
     273                if ( true === $rotated && ! empty( $image_meta['image_meta']['orientation'] ) ) {
     274                    $image_meta['image_meta']['orientation'] = 1;
     275                }
     276            } else {
     277                // TODO: handle errors.
     278            }
     279        } else {
     280            // TODO: handle errors.
     281        }
     282    } elseif ( ! empty( $exif_meta['orientation'] ) && (int) $exif_meta['orientation'] !== 1 ) {
     283        // Rotate the whole original image if there is EXIF data and "orientation" is not 1.
     284
     285        $editor = wp_get_image_editor( $file );
     286
     287        if ( is_wp_error( $editor ) ) {
     288            // This image cannot be edited.
     289            return $image_meta;
     290        }
     291
     292        // Rotate the image
     293        $rotated = $editor->maybe_exif_rotate();
     294
     295        if ( true === $rotated ) {
     296            // Append `-rotated` to the image file name.
     297            $saved = $editor->save( $editor->generate_filename( 'rotated' ) );
     298
     299            if ( ! is_wp_error( $saved ) ) {
     300                $image_meta = _wp_image_meta_replace_original( $saved, $file, $image_meta, $attachment_id );
     301
     302                // Update the stored EXIF data.
     303                if ( ! empty( $image_meta['image_meta']['orientation'] ) ) {
     304                    $image_meta['image_meta']['orientation'] = 1;
     305                }
     306            } else {
     307                // TODO: handle errors.
    249308            }
    250309        }
     
    326385        // The image cannot be edited.
    327386        return $image_meta;
     387    }
     388
     389    // If stored EXIF data exists, rotate the source image before creating sub-sizes.
     390    if ( ! empty( $image_meta['image_meta'] ) ) {
     391        $rotated = $editor->maybe_exif_rotate();
     392
     393        if ( is_wp_error( $rotated ) ) {
     394            // TODO: handle errors.
     395        }
    328396    }
    329397
  • trunk/src/wp-includes/class-wp-image-editor-imagick.php

    r46088 r46202  
    567567            $this->image->rotateImage( new ImagickPixel( 'none' ), 360 - $angle );
    568568
    569             // Normalise Exif orientation data so that display is consistent across devices.
     569            // Normalise EXIF orientation data so that display is consistent across devices.
    570570            if ( is_callable( array( $this->image, 'setImageOrientation' ) ) && defined( 'Imagick::ORIENTATION_TOPLEFT' ) ) {
    571571                $this->image->setImageOrientation( Imagick::ORIENTATION_TOPLEFT );
     
    603603                $this->image->flopImage();
    604604            }
     605
     606            // Normalise EXIF orientation data so that display is consistent across devices.
     607            if ( is_callable( array( $this->image, 'setImageOrientation' ) ) && defined( 'Imagick::ORIENTATION_TOPLEFT' ) ) {
     608                $this->image->setImageOrientation( Imagick::ORIENTATION_TOPLEFT );
     609            }
    605610        } catch ( Exception $e ) {
    606611            return new WP_Error( 'image_flip_error', $e->getMessage() );
    607612        }
     613
    608614        return true;
     615    }
     616
     617    /**
     618     * Check if a JPEG image has EXIF Orientation tag and rotate it if needed.
     619     *
     620     * As ImageMagick copies the EXIF data to the flipped/rotated image, proceed only
     621     * if EXIF Orientation can be reset afterwards.
     622     *
     623     * @since 5.3.0
     624     *
     625     * @return bool|WP_Error True if the image was rotated. False if no EXIF data or if the image doesn't need rotation.
     626     *                       WP_Error if error while rotating.
     627     */
     628    public function maybe_exif_rotate() {
     629        if ( is_callable( array( $this->image, 'setImageOrientation' ) ) && defined( 'Imagick::ORIENTATION_TOPLEFT' ) ) {
     630            return parent::maybe_exif_rotate();
     631        } else {
     632            return new WP_Error( 'write_exif_error', __( 'The image cannot be rotated because the embedded meta data cannot be updated.' ) );
     633        }
    609634    }
    610635
  • trunk/src/wp-includes/class-wp-image-editor.php

    r45590 r46202  
    386386
    387387    /**
     388     * Check if a JPEG image has EXIF Orientation tag and rotate it if needed.
     389     *
     390     * @since 5.3.0
     391     *
     392     * @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).
     393     *                       WP_Error if error while rotating.
     394     */
     395    public function maybe_exif_rotate() {
     396        $orientation = null;
     397
     398        if ( is_callable( 'exif_read_data' ) && 'image/jpeg' === $this->mime_type ) {
     399            $exif_data = @exif_read_data( $this->file );
     400
     401            if ( ! empty( $exif_data['Orientation'] ) ) {
     402                $orientation = (int) $exif_data['Orientation'];
     403            }
     404        }
     405
     406        /**
     407         * Filters the `$orientation` value to correct it before rotating or to prevemnt rotating the image.
     408         *
     409         * @since 5.3.0
     410         *
     411         * @param int    $orientation EXIF Orientation value as retrieved from the image file.
     412         * @param string $file        Path to the image file.
     413         */
     414        $orientation = apply_filters( 'wp_image_maybe_exif_rotate', $orientation, $this->file );
     415
     416        if ( ! $orientation || $orientation === 1 ) {
     417            return false;
     418        }
     419
     420        switch ( $orientation ) {
     421            case 2:
     422                // Flip horizontally.
     423                $result = $this->flip( true, false );
     424                break;
     425            case 3:
     426                // Rotate 180 degrees or flip horizontally and vertically.
     427                // Flipping seems faster/uses less resources.
     428                $result = $this->flip( true, true );
     429                break;
     430            case 4:
     431                // Flip vertically.
     432                $result = $this->flip( false, true );
     433                break;
     434            case 5:
     435                // Rotate 90 degrees counter-clockwise and flip vertically.
     436                $result = $this->rotate( 90 );
     437
     438                if ( ! is_wp_error( $result ) ) {
     439                    $result = $this->flip( false, true );
     440                }
     441
     442                break;
     443            case 6:
     444                // Rotate 90 degrees clockwise (270 counter-clockwise).
     445                $result = $this->rotate( 270 );
     446                break;
     447            case 7:
     448                // Rotate 90 degrees counter-clockwise and flip horizontally.
     449                $result = $this->rotate( 90 );
     450
     451                if ( ! is_wp_error( $result ) ) {
     452                    $result = $this->flip( true, false );
     453                }
     454
     455                break;
     456            case 8:
     457                // Rotate 90 degrees counter-clockwise.
     458                $result = $this->rotate( 90 );
     459                break;
     460        }
     461
     462        return $result;
     463    }
     464
     465    /**
    388466     * Either calls editor's save function or handles file as a stream.
    389467     *
Note: See TracChangeset for help on using the changeset viewer.