WordPress.org

Make WordPress Core

Ticket #14459: 14459.3.diff

File 14459.3.diff, 10.7 KB (added by pbiron, 4 months ago)
  • src/wp-includes/class-wp-image-editor-gd.php

    From d5cf6ccacbc13989081b66577aab36fc52653f1b Mon Sep 17 00:00:00 2001
    From: Paul Biron <paul@sparrowhawkcomputing.com>
    Date: Sun, 28 Jul 2019 14:12:18 -0600
    Subject: [PATCH] Preserve JPEG application segments from uploaded images when
     using GD image editor and rotate JPEG images upon upload if the EXIF
     orientation tag indicates it is warranted.
    
    ---
     src/wp-includes/class-wp-image-editor-gd.php | 195 +++++++++++++++++++
     src/wp-includes/default-filters.php          |   2 +
     src/wp-includes/media.php                    |  80 ++++++++
     3 files changed, 277 insertions(+)
    
    diff --git a/src/wp-includes/class-wp-image-editor-gd.php b/src/wp-includes/class-wp-image-editor-gd.php
    index daec6b3d04..d809ebef67 100644
    a b  
    1414 * @see WP_Image_Editor
    1515 */
    1616class WP_Image_Editor_GD extends WP_Image_Editor {
     17        /**
     18         * EXIF little endian byte order mark.
     19         *
     20         * @since 5.3.0
     21         *
     22         * @var string
     23         */
     24        const EXIF_LITTLE_ENDIAN      = 'II';
     25
     26        /**
     27         * EXIF Orientation tag ID.
     28         *
     29         * @since 5.3.0
     30         *
     31         * @var int
     32         */
     33        const EXIF_TAG_ORIENTATION    = 0x112;
     34
     35        /**
     36         * EXIF "normal" Orientation value.
     37         *
     38         * @since 5.3.0
     39         * .
     40         * @var int
     41         */
     42        const EXIF_ORIENTATION_NORMAL = 1;
     43
    1744        /**
    1845         * GD Resource.
    1946         *
    class WP_Image_Editor_GD extends WP_Image_Editor { 
    2148         */
    2249        protected $image;
    2350
     51        /**
     52         * Whether the image has been rotated.
     53         *
     54         * @var bool
     55         */
     56        protected $rotated = false;
     57
    2458        public function __destruct() {
    2559                if ( $this->image ) {
    2660                        // we don't need the original in memory anymore
    class WP_Image_Editor_GD extends WP_Image_Editor { 
    426460                                return new WP_Error( 'image_save_error', __( 'Image Editor Save Failed' ) );
    427461                        }
    428462                } elseif ( 'image/jpeg' === $mime_type ) {
     463                        // temporarily store JPEG application segments from the current image.
     464                        @getimagesize( $this->file, $imageinfo );
     465
    429466                        if ( ! $this->make_image( $filename, 'imagejpeg', array( $image, $filename, $this->get_quality() ) ) ) {
    430467                                return new WP_Error( 'image_save_error', __( 'Image Editor Save Failed' ) );
    431468                        }
     469
     470                        // copy the original JPEG application segments to the file created by GD.
     471                        $this->copy_jpeg_segments( $filename, $imageinfo );
    432472                } else {
    433473                        return new WP_Error( 'image_save_error', __( 'Image Editor Save Failed' ) );
    434474                }
    class WP_Image_Editor_GD extends WP_Image_Editor { 
    495535
    496536                return parent::make_image( $filename, $function, $arguments );
    497537        }
     538
     539        /**
     540         * Copy JPEG application segments from source image to destination image.
     541         *
     542         * {@link https://www.php.net/manual/en/book.image.php GD} strips JPEG
     543         * application segments when it saves images.  This method can be used to
     544         * copy any JPEG application segments present in an "original" image to
     545         * one being savevd by `GD`.
     546         *
     547         * @since 5.3.0
     548         *
     549         * @param string $filename The filename to copy the JPEG segments to.
     550         * @param array $imageinfo JPEG application segments (extracted with
     551         *                         {@link https://www.php.net/manual/en/function.getimagesize.php getimagesize()}).
     552         * @return int|bool The number of bytes that were written to the file or false on failure.
     553         */
     554        protected function copy_jpeg_segments( $filename, $imageinfo ) {
     555                $segment_markers = array(
     556                        'APP1'  => 0xE1, // EXIF.
     557                        'APP2'  => 0xE2, // ICC Profile.
     558                        'APP13' => 0xED, // IPTC.
     559                );
     560                $segments = array();
     561
     562                if ( $this->rotated && isset( $imageinfo['APP1'] ) ) {
     563                        // the image has been rotated by GD, so set the EXIF orientation to "Normal".
     564                        $imageinfo['APP1'] = $this->set_exif_orientation(
     565                                self::EXIF_ORIENTATION_NORMAL,
     566                                $imageinfo['APP1']
     567                        );
     568                }
     569
     570                foreach ( $imageinfo as $segment_marker => $segment_data ) {
     571                        $segment_length = strlen( $segment_data ) + 2;
     572                        if ( in_array( $segment_marker, array_keys( $segment_markers ) ) && $segment_length > 0xFFFF ) {
     573                                // @todo do we really want to bail in this case?
     574                                //       Maybe we should just skip this segment.  This return was in
     575                                //       the gist by @n7studios.
     576                                return false;
     577                        }
     578
     579                        switch ( $segment_marker ) {
     580                                case 'APP1':  // EXIF.
     581                                case 'APP2':  // ICC Profile.
     582                                        if ( 'ICC_PROFILE' !== substr( $segment_data, 0, 11 ) ) {
     583                                                // something besides an ICC Profile is using the APP2 segment, so skip it.
     584                                                // see Annex B.4 of @link http://www.color.org/specification/ICC1v43_2010-12.pdf
     585                                                // @todo should we do a similar check for APP1 and APP13?  I'm not sure how
     586                                                //       we'd peform that check
     587                                                continue;
     588                                        }
     589                                        // fall through.
     590                                case 'APP13': // IPTC.
     591                                        $segments[ $segment_marker ] =
     592                                        // segment marker.
     593                                        chr( 0xFF ) . chr( $segment_markers[ $segment_marker ] ) .
     594                                        // segment length.
     595                                        chr( ( $segment_length >> 8 ) & 0xFF) . chr( $segment_length & 0xFF ) .
     596                                        // segment data.
     597                                        $segment_data;
     598
     599                                        break;
     600                        }
     601                }
     602
     603                $image_data = file_get_contents( $filename );
     604                // skip the JFIF "header".
     605                $image_data = substr( $image_data, 2 );
     606
     607                // Variable accumulates new JPEG segments, intialized with JFIF "header".
     608                $portion_to_add = chr( 0xFF ) . chr( 0xD8 );
     609
     610                foreach ( $segments as $segment_data ) {
     611                        $portion_to_add .= $segment_data;
     612                }
     613
     614                return file_put_contents( $filename, $portion_to_add . $image_data );
     615        }
     616
     617        /**
     618         * Set the EXIF ORIENTATION.
     619         *
     620         * @since 5.3.0
     621         *
     622         * @param int $orientation The EXIF orientation.  Possible values are an integer
     623         *                         between 1 and 8 (inclusive).
     624         * @param string $exif_data The binary EXIF data.
     625         * @return string The altered binary EXIF data.
     626         */
     627        protected function set_exif_orientation( $orientation, $exif_data ) {
     628                if ( 0 < $orientation || $orientation > 8 ) {
     629                        // invalid orientation.
     630                        return $exif_data;
     631                }
     632
     633                // EXIF data is stored as a TIFF segment, and hence has a btye order.
     634                // 'II' (0x4949) is little endian (also known as "Intel format").
     635                // 'MM' (0x4D4D) is big endian (also known as "Motorola format").
     636                $exif_byteorder = substr( $exif_data, 6, 2 );
     637
     638                // the number of EXIF entries starts at offset 14.
     639                $offset      = 14;
     640                $num_entries = $this->get_exif_short( $offset, $exif_data, $exif_byteorder );
     641
     642                // advance the offset passed the number of entries data.
     643                $offset = 16;
     644                for ( $i = 0; $i < $num_entries; $i ++ ) {
     645                        // each EXIF tag entry is 12 bytes, hence the "12 * $i"
     646                        $tag = $this->get_exif_short( $offset + ( 12 * $i ), $exif_data, $exif_byteorder );
     647
     648                        if ( self::EXIF_TAG_ORIENTATION === $tag ) {
     649                                // the +8 below is to skip over the tag, type, and count associated with
     650                                // the orientation entry.
     651                                if ( self::EXIF_LITTLE_ENDIAN === $exif_byteorder ) {
     652                                        $exif_data{ $offset + ( 12 * $i ) + 8 + 1 } = chr( 0 );
     653                                        $exif_data{ $offset + ( 12 * $i ) + 8     } = chr( $orientation );
     654                                }
     655                                else {
     656                                        // big endian.
     657                                        $exif_data{ $offset + ( 12 * $i ) + 8     } = chr( 0 );
     658                                        $exif_data{ $offset + ( 12 * $i ) + 8 + 1 } = chr( $orientation );
     659                                }
     660
     661                                break;
     662                        }
     663                }
     664
     665                return $exif_data;
     666        }
     667
     668        /**
     669         * Get a short (2 byte) value from EXIF data.
     670         *
     671         * @since 5.3.0
     672         *
     673         * @param int $offset The offset into the EXIF data where the value is stored.
     674         * @param string $exif_data The binary EXIF data.
     675         * @param string $exif_byteorder The byte order of the binary EXIF data.
     676         *                               'II' (0x4949) for little endian and
     677         *                               'MM' (0x4D4D) for big endian.
     678         * @return mixed
     679         */
     680        protected function get_exif_short( $offset, $exif_data, $exif_byteorder ) {
     681                if ( self::EXIF_LITTLE_ENDIAN === $exif_byteorder ) {
     682                        return
     683                                ord( $exif_data{ $offset + 1 } ) * 256 +
     684                                ord( $exif_data{ $offset } );
     685                }
     686                else {
     687                        // big endian.
     688                        return
     689                                ord( $exif_data{ $offset } )     * 256 +
     690                                ord( $exif_data{ $offset + 1 } );
     691                }
     692        }
    498693}
  • src/wp-includes/default-filters.php

    diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php
    index 056e8ffb43..04e04d8813 100644
    a b add_action( 'auth_cookie_bad_hash', 'rest_cookie_collect_status' ); 
    277277add_action( 'auth_cookie_valid', 'rest_cookie_collect_status' );
    278278add_filter( 'rest_authentication_errors', 'rest_cookie_check_errors', 100 );
    279279
     280add_filter( 'wp_handle_upload', 'maybe_rotate_jpeg_on_upload' );
     281
    280282// Actions
    281283add_action( 'wp_head', '_wp_render_title_tag', 1 );
    282284add_action( 'wp_head', 'wp_enqueue_scripts', 1 );
  • src/wp-includes/media.php

    diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php
    index 1cf3d3ff0b..82c17ff8e5 100644
    a b function wp_media_personal_data_exporter( $email_address, $page = 1 ) { 
    43224322                'done' => $done,
    43234323        );
    43244324}
     4325
     4326
     4327/**
     4328 * Rotate JPEG images upon upload according to their EXIF Orientation tag.
     4329 *
     4330 * @since 5.3.0
     4331 *
     4332 * @param array $upload {
     4333 *     Array of upload data.
     4334 *
     4335 *     @type string $file Filename of the newly-uploaded file.
     4336 *     @type string $url URL of the uploaded file.
     4337 *     @type string $type File type.
     4338 * }
     4339 * @return array
     4340 */
     4341function maybe_rotate_jpeg_on_upload( $upload ) {
     4342        // Check that uploaded file exists.
     4343        if ( ! file_exists( $upload['file'] ) ) {
     4344                return $upload;
     4345        }
     4346
     4347        // Check that uploaded file is a JPEG image file.
     4348        if ( 'image/jpeg' !== $upload['type'] ) {
     4349                return $upload;
     4350        }
     4351
     4352        // Attempt to read EXIF data from the image.
     4353        if ( ! function_exists( 'wp_read_image_metadata' ) ) {
     4354                require_once ABSPATH . '/wp-admin/includes/image.php';
     4355        }
     4356        $exif_metadata = wp_read_image_metadata( $upload['file'] );
     4357        if ( ! $exif_metadata ) {
     4358                return $upload;
     4359        }
     4360
     4361        // Check if EXIF orientation tag exists.
     4362        if ( ! isset( $exif_metadata['orientation'] ) ) {
     4363                return $upload;
     4364        }
     4365
     4366        // Check if the orientation is one we're looking for.
     4367        $required_orientations = array(
     4368        3, // Rotate 180 degrees.
     4369        6, // Rotate 270 degrees counter-clockwise.
     4370        8, // Rotate 90 degrees counter-clockwise.
     4371        );
     4372        if ( ! in_array( $exif_metadata['orientation'], $required_orientations ) ) {
     4373                return $upload;
     4374        }
     4375
     4376        // If here, orientation tag value is one we're looking for.
     4377        // Load the WordPress Image Editor class.
     4378        $image = wp_get_image_editor( $upload['file'] );
     4379        if ( is_wp_error( $image ) ) {
     4380                // Something went wrong - abort.
     4381                return $upload;
     4382        }
     4383
     4384        // Rotate the image according to the EXIF orientation.
     4385        switch ( $exif_metadata['orientation'] ) {
     4386                case 3:
     4387                        $image->rotate( 180 );
     4388
     4389                        break;
     4390                case 6:
     4391                        $image->rotate( 270 );
     4392
     4393                        break;
     4394                case 8:
     4395                        $image->rotate( 90 );
     4396
     4397                        break;
     4398        }
     4399
     4400        // Save the image, overwriting the existing image.
     4401        $image->save( $upload['file'] );
     4402
     4403        return $upload;
     4404}