Ticket #14459: 14459.3.diff
File 14459.3.diff, 10.7 KB (added by , 6 years 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 14 14 * @see WP_Image_Editor 15 15 */ 16 16 class 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 17 44 /** 18 45 * GD Resource. 19 46 * … … class WP_Image_Editor_GD extends WP_Image_Editor { 21 48 */ 22 49 protected $image; 23 50 51 /** 52 * Whether the image has been rotated. 53 * 54 * @var bool 55 */ 56 protected $rotated = false; 57 24 58 public function __destruct() { 25 59 if ( $this->image ) { 26 60 // we don't need the original in memory anymore … … class WP_Image_Editor_GD extends WP_Image_Editor { 426 460 return new WP_Error( 'image_save_error', __( 'Image Editor Save Failed' ) ); 427 461 } 428 462 } elseif ( 'image/jpeg' === $mime_type ) { 463 // temporarily store JPEG application segments from the current image. 464 @getimagesize( $this->file, $imageinfo ); 465 429 466 if ( ! $this->make_image( $filename, 'imagejpeg', array( $image, $filename, $this->get_quality() ) ) ) { 430 467 return new WP_Error( 'image_save_error', __( 'Image Editor Save Failed' ) ); 431 468 } 469 470 // copy the original JPEG application segments to the file created by GD. 471 $this->copy_jpeg_segments( $filename, $imageinfo ); 432 472 } else { 433 473 return new WP_Error( 'image_save_error', __( 'Image Editor Save Failed' ) ); 434 474 } … … class WP_Image_Editor_GD extends WP_Image_Editor { 495 535 496 536 return parent::make_image( $filename, $function, $arguments ); 497 537 } 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 } 498 693 } -
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' ); 277 277 add_action( 'auth_cookie_valid', 'rest_cookie_collect_status' ); 278 278 add_filter( 'rest_authentication_errors', 'rest_cookie_check_errors', 100 ); 279 279 280 add_filter( 'wp_handle_upload', 'maybe_rotate_jpeg_on_upload' ); 281 280 282 // Actions 281 283 add_action( 'wp_head', '_wp_render_title_tag', 1 ); 282 284 add_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 ) { 4322 4322 'done' => $done, 4323 4323 ); 4324 4324 } 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 */ 4341 function 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 }