Make WordPress Core

Changeset 38086


Ignore:
Timestamp:
07/18/2016 02:13:45 AM (8 years ago)
Author:
joemcgill
Message:

Media: Prevent image_get_intermediate_size() from returning cropped images.

When $size is passed to image_get_intermediate_size() as an array of width
and height values and an exact image size matching those values isn't available,
the function loops through the available attachment sizes and returns the
smallest image larger than the requested dimensions with the same aspect ratio.

The aspect ratio check is skipped for the 'thumbnail' size to provide a fallback
for small sizes when no other image option is available. This resulted in a poor
selection when the size requested was smaller than the 'thumbnail' dimensions
but a larger size matching the requested ratio existed.

This refactors the internals of image_get_intermediate_size() to ensure the
'thumbnail' size is only returned as a fallback to small sizes once all other
options have been considered, and makes the control flow easier to follow.

This also introduces a new helper function, wp_image_matches_ratio() for
testing whether the aspect ratios of two sets of dimensions match. This function
is also now used in wp_calculate_image_srcset() during the selection process.

Props flixos, joemcgill.
Fixes #34384, #34980.

Location:
trunk
Files:
2 edited

Legend:

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

    r38052 r38086  
    593593
    594594/**
     595 * Helper function to test if aspect ratios for two images match.
     596 *
     597 * @since 4.6.0
     598 *
     599 * @param int $source_width  Width of the first image in pixels.
     600 * @param int $source_height Height of the first image in pixels.
     601 * @param int $target_width  Width of the second image in pixels.
     602 * @param int $target_height Height of the second image in pixels.
     603 * @return bool True if aspect ratios match within 1px. False if not.
     604 */
     605function wp_image_matches_ratio( $source_width, $source_height, $target_width, $target_height ) {
     606    /*
     607     * To test for varying crops, we constrain the dimensions of the larger image
     608     * to the dimensions of the smaller image and see if they match.
     609     */
     610    if ( $source_width > $target_width ) {
     611        $constrained_size = wp_constrain_dimensions( $source_width, $source_height, $target_width );
     612        $expected_size = array( $target_width, $target_height );
     613    } else {
     614        $constrained_size = wp_constrain_dimensions( $target_width, $target_height, $source_width );
     615        $expected_size = array( $source_width, $source_height );
     616    }
     617
     618    // If the image dimensions are within 1px of the expected size, we consider it a match.
     619    $matched = ( abs( $constrained_size[0] - $expected_size[0] ) <= 1 && abs( $constrained_size[1] - $expected_size[1] ) <= 1 );
     620
     621    return $matched;
     622}
     623
     624/**
    595625 * Retrieves the image's intermediate size (resized) path, width, and height.
    596626 *
     
    624654 *     @type int    $width  Width of image
    625655 *     @type int    $height Height of image
    626  *     @type string $path   Optional. Image's absolute filesystem path. Only returned if registered
    627  *                          size is passed to `$size` parameter.
    628  *     @type string $url    Optional. Image's URL. Only returned if registered size is passed to `$size`
    629  *                          parameter.
     656 *     @type string $path   Image's absolute filesystem path.
     657 *     @type string $url    Image's URL.
    630658 * }
    631659 */
    632660function image_get_intermediate_size( $post_id, $size = 'thumbnail' ) {
    633     if ( !is_array( $imagedata = wp_get_attachment_metadata( $post_id ) ) )
     661    if ( ! $size || ! is_array( $imagedata = wp_get_attachment_metadata( $post_id ) ) || empty( $imagedata['sizes'] )  ) {
    634662        return false;
    635 
    636     // get the best one for a specified set of dimensions
    637     if ( is_array($size) && !empty($imagedata['sizes']) ) {
     663    }
     664
     665    $data = array();
     666
     667    // Find the best match when '$size' is an array.
     668    if ( is_array( $size ) ) {
    638669        $candidates = array();
    639670
     
    641672            // If there's an exact match to an existing image size, short circuit.
    642673            if ( $data['width'] == $size[0] && $data['height'] == $size[1] ) {
    643                 list( $data['width'], $data['height'] ) = image_constrain_size_for_editor( $data['width'], $data['height'], $size );
    644 
    645                 /** This filter is documented in wp-includes/media.php */
    646                 return apply_filters( 'image_get_intermediate_size', $data, $post_id, $size );
     674                $candidates[ $data['width'] * $data['height'] ] = $data;
     675                break;
    647676            }
    648             // If it's not an exact match but it's at least the dimensions requested.
     677
     678            // If it's not an exact match, consider larger sizes with the same aspect ratio.
    649679            if ( $data['width'] >= $size[0] && $data['height'] >= $size[1] ) {
    650                 $candidates[ $data['width'] * $data['height'] ] = $_size;
     680                // If '0' is passed to either size, we test ratios against the original file.
     681                if ( 0 === $size[0] || 0 === $size[1] ) {
     682                    $same_ratio = wp_image_matches_ratio( $data['width'], $data['height'], $imagedata['width'], $imagedata['height'] );
     683                } else {
     684                    $same_ratio = wp_image_matches_ratio( $data['width'], $data['height'], $size[0], $size[1] );
     685                }
     686
     687                if ( $same_ratio ) {
     688                    $candidates[ $data['width'] * $data['height'] ] = $data;
     689                }
    651690            }
    652691        }
    653692
    654693        if ( ! empty( $candidates ) ) {
    655             // find for the smallest image not smaller than the desired size
    656             ksort( $candidates );
    657             foreach ( $candidates as $_size ) {
    658                 $data = $imagedata['sizes'][$_size];
    659 
    660                 // Skip images with unexpectedly divergent aspect ratios (crops)
    661                 // First, we calculate what size the original image would be if constrained to a box the size of the current image in the loop
    662                 $maybe_cropped = image_resize_dimensions($imagedata['width'], $imagedata['height'], $data['width'], $data['height'], false );
    663                 // If the size doesn't match within one pixel, then it is of a different aspect ratio, so we skip it, unless it's the thumbnail size
    664                 if ( 'thumbnail' != $_size &&
    665                   ( ! $maybe_cropped
    666                     || ( $maybe_cropped[4] != $data['width'] && $maybe_cropped[4] + 1 != $data['width'] )
    667                     || ( $maybe_cropped[5] != $data['height'] && $maybe_cropped[5] + 1 != $data['height'] )
    668                   ) ) {
    669                   continue;
    670                 }
    671                 // If we're still here, then we're going to use this size.
    672                 list( $data['width'], $data['height'] ) = image_constrain_size_for_editor( $data['width'], $data['height'], $size );
    673 
    674                 /** This filter is documented in wp-includes/media.php */
    675                 return apply_filters( 'image_get_intermediate_size', $data, $post_id, $size );
     694            // Sort the array by size if we have more than one candidate.
     695            if ( 1 < count( $candidates ) ) {
     696                ksort( $candidates );
    676697            }
    677         }
    678     }
    679 
    680     if ( is_array($size) || empty($size) || empty($imagedata['sizes'][$size]) )
     698
     699            $data = array_shift( $candidates );
     700        /*
     701         * When the size requested is smaller than the thumbnail dimensions, we
     702         * fall back to the thumbnail size to maintain backwards compatibility with
     703         * pre 4.6 versions of WordPress.
     704         */
     705        } elseif ( ! empty( $imagedata['sizes']['thumbnail'] ) && $imagedata['sizes']['thumbnail']['width'] >= $size[0] && $imagedata['sizes']['thumbnail']['width'] >= $size[1] ) {
     706            $data = $imagedata['sizes']['thumbnail'];
     707        } else {
     708            return false;
     709        }
     710
     711        // Constrain the width and height attributes to the requested values.
     712        list( $data['width'], $data['height'] ) = image_constrain_size_for_editor( $data['width'], $data['height'], $size );
     713
     714    } elseif ( ! empty( $imagedata['sizes'][ $size ] ) ) {
     715        $data = $imagedata['sizes'][ $size ];
     716    }
     717
     718    // If we still don't have a match at this point, return false.
     719    if ( empty( $data ) ) {
    681720        return false;
    682 
    683     $data = $imagedata['sizes'][$size];
     721    }
     722
    684723    // include the full filesystem path of the intermediate file
    685724    if ( empty($data['path']) && !empty($data['file']) ) {
     
    10931132        }
    10941133
    1095         /**
    1096          * To check for varying crops, we calculate the expected size of the smaller
    1097          * image if the larger were constrained by the width of the smaller and then
    1098          * see if it matches what we're expecting.
    1099          */
    1100         if ( $image_width > $image['width'] ) {
    1101             $constrained_size = wp_constrain_dimensions( $image_width, $image_height, $image['width'] );
    1102             $expected_size = array( $image['width'], $image['height'] );
    1103         } else {
    1104             $constrained_size = wp_constrain_dimensions( $image['width'], $image['height'], $image_width );
    1105             $expected_size = array( $image_width, $image_height );
    1106         }
    1107 
    11081134        // If the image dimensions are within 1px of the expected size, use it.
    1109         if ( abs( $constrained_size[0] - $expected_size[0] ) <= 1 && abs( $constrained_size[1] - $expected_size[1] ) <= 1 ) {
     1135        if ( wp_image_matches_ratio( $image_width, $image_height, $image['width'], $image['height'] ) ) {
    11101136            // Add the URL, descriptor, and value to the sources array to be returned.
    11111137            $source = array(
  • trunk/tests/phpunit/tests/image/intermediate_size.php

    r37328 r38086  
    225225        $this->assertTrue( strpos( $image['file'], $width . 'x' . $height ) > 0 );
    226226    }
     227
     228    /**
     229     * @ticket 34384
     230     */
     231    public function test_get_intermediate_size_with_small_size_array() {
     232        // Add a hard cropped size that matches the aspect ratio we're going to test.
     233        add_image_size( 'test-size', 200, 100, true );
     234
     235        $file = DIR_TESTDATA . '/images/waffles.jpg';
     236        $id = $this->_make_attachment( $file, 0 );
     237
     238        // Request a size by array that doesn't exist and is smaller than the 'thumbnail'
     239        $image = image_get_intermediate_size( $id, array( 50, 25 ) );
     240
     241        // We should get the 'test-size' file and not the thumbnail.
     242        $this->assertTrue( strpos( $image['file'], '200x100' ) > 0 );
     243    }
     244
     245    /**
     246     * @ticket 34384
     247     */
     248    public function test_get_intermediate_size_with_small_size_array_fallback() {
     249        $file = DIR_TESTDATA . '/images/waffles.jpg';
     250        $id = $this->_make_attachment( $file, 0 );
     251
     252        $original = wp_get_attachment_metadata( $id );
     253        $thumbnail_file = $original['sizes']['thumbnail']['file'];
     254
     255        // Request a size by array that doesn't exist and is smaller than the 'thumbnail'
     256        $image = image_get_intermediate_size( $id, array( 50, 25 ) );
     257
     258        // We should get the 'thumbnail' file as a fallback.
     259        $this->assertSame( $image['file'], $thumbnail_file );
     260    }
    227261}
Note: See TracChangeset for help on using the changeset viewer.