Responsive images:

- Introduce `wp_calculate_image_srcset()` that replaces `wp_get_attachment_image_srcset_array()` and is used as lower level function for retrieving the srcset data as array.
- Use the new function when generating `srcset` and `sizes` on the front-end. This is faster as no (other) image API functions are used.
- Change the `wp_get_attachment_image_srcset()`. Now it is meant for use in templates and is no longer used in core.
- A few logic fixes and improvements.
- Some names changed to be (hopefully) more descriptive.
- Fixed/updated tests.

Props joemcgill, jaspermdegroot, azaozz.
See #34430.
Built from https://develop.svn.wordpress.org/trunk@35412


git-svn-id: http://core.svn.wordpress.org/trunk@35376 1a063a9b-81f0-0310-95a4-ce76da25c4cd
This commit is contained in:
Andrew Ozz 2015-10-28 05:41:24 +00:00
parent 8d0f76bc11
commit 61d21cc0d9
2 changed files with 251 additions and 179 deletions

View File

@ -813,13 +813,14 @@ function wp_get_attachment_image($attachment_id, $size = 'thumbnail', $icon = fa
// Generate srcset and sizes if not already present.
if ( empty( $attr['srcset'] ) ) {
$srcset = wp_get_attachment_image_srcset( $attachment_id, $size );
$sizes = wp_get_attachment_image_sizes( $attachment_id, $size, $width );
$image_meta = wp_get_attachment_metadata( $attachment_id );
$size_array = array( absint( $width ), absint( $height ) );
$sources = wp_calculate_image_srcset( $src, $size_array, $image_meta, $attachment_id );
if ( $srcset && $sizes ) {
$attr['srcset'] = $srcset;
if ( count( $sources ) > 1 ) {
$attr['srcset'] = wp_image_srcset_attr( $sources, $size_array, $image_meta, $attachment_id );
if ( empty( $attr['sizes'] ) ) {
if ( empty( $attr['sizes'] ) && ( $sizes = wp_get_attachment_image_sizes( $size_array, $image_meta, $attachment_id ) ) ) {
$attr['sizes'] = $sizes;
}
}
@ -864,15 +865,118 @@ function wp_get_attachment_image_url( $attachment_id, $size = 'thumbnail', $icon
}
/**
* Retrieves an array of URLs and pixel widths representing sizes of an image.
*
* The purpose is to populate a source set when creating responsive image markup.
* Private, do not use
*/
function _wp_upload_dir_baseurl() {
static $baseurl = null;
if ( ! $baseurl ) {
$uploads_dir = wp_upload_dir();
$baseurl = $uploads_dir['baseurl'];
}
return $baseurl;
}
/**
* Private, do not use
*/
function _wp_get_image_size_from_meta( $size, $image_meta ) {
if ( $size === 'full' ) {
return array(
absint( $image_meta['width'] ),
absint( $image_meta['height'] ),
);
} elseif ( ! empty( $image_meta['sizes'][$size] ) ) {
return array(
absint( $image_meta['sizes'][$size]['width'] ),
absint( $image_meta['sizes'][$size]['height'] ),
);
}
return false;
}
/**
* Retrieves the value for an image attachment's 'srcset' attribute.
*
* @since 4.4.0
*
* @param int $attachment_id Image attachment ID.
* @param int $attachment_id Optional. Image attachment ID.
* @param array|string $size Image size. Accepts any valid image size, or an array of width and height
* values in pixels (in that order). Default 'medium'.
* @param array $image_meta Optional. The image meta data.
* @return string|bool A 'srcset' value string or false.
*/
function wp_get_attachment_image_srcset( $attachment_id, $size = 'medium', $image_meta = null ) {
if ( ! $image = wp_get_attachment_image_src( $attachment_id, $size ) ) {
return false;
}
if ( ! is_array( $image_meta ) ) {
$image_meta = get_post_meta( $attachment_id, '_wp_attachment_metadata', true );
}
$image_url = $image[0];
$size_array = array(
absint( $image[1] ),
absint( $image[2] )
);
// Calculate the sources for the srcset.
$sources = wp_calculate_image_srcset( $image_url, $size_array, $image_meta, $attachment_id );
// Only return a srcset value if there is more than one source.
if ( count( $sources ) < 2 ) {
return false;
}
return wp_image_srcset_attr( $sources, $size_array, $image_meta, $attachment_id );
}
/**
* A helper function to concatenate and filter the srcset attribute value.
*
* @since 4.4.0
*
* @param array $sources The array containing image sizes data as returned by wp_calculate_image_srcset().
* @param array $size_array Array of width and height values in pixels (in that order).
* @param array $image_meta The image meta data.
* @param int $attachment_id The image attachment post id to pass to the filter.
* @return string The srcset attribute value.
*/
function wp_image_srcset_attr( $sources, $size_array, $image_meta, $attachment_id ) {
$srcset = '';
foreach ( $sources as $source ) {
$srcset .= $source['url'] . ' ' . $source['value'] . $source['descriptor'] . ', ';
}
/**
* Filter the output of wp_get_attachment_image_srcset().
*
* @since 4.4.0
*
* @param string $srcset A source set formatted for a `srcset` attribute.
* @param int $attachment_id Image attachment ID.
* @param array|string $size Image size. Accepts any valid image size, or an array of width and height
* values in pixels (in that order). Default 'medium'.
* @param array $image_meta The image meta data.
*/
return apply_filters( 'wp_get_attachment_image_srcset', rtrim( $srcset, ', ' ), $attachment_id, $size_array, $image_meta );
}
/**
* A helper function to caclulate the image sources to include in a srcset attribute.
*
* @since 4.4.0
*
* @param string $image_name The file name, path, URL or partial path or URL of the image being matched.
* @param array $size_array Array of width and height values in pixels (in that order).
* @param array $image_meta The image meta data.
* @param int $attachment_id Optional. The image attachment post id to pass to the filter.
* @return array|bool $sources {
* Array image candidate values containing a URL, descriptor type, and
* descriptor value. False if none exist.
@ -886,103 +990,91 @@ function wp_get_attachment_image_url( $attachment_id, $size = 'thumbnail', $icon
* }
*
*/
function wp_get_attachment_image_srcset_array( $attachment_id, $size = 'medium' ) {
// Get the intermediate size.
$image = image_get_intermediate_size( $attachment_id, $size );
// Get the post meta.
$img_meta = wp_get_attachment_metadata( $attachment_id );
if ( ! is_array( $img_meta ) ) {
function wp_calculate_image_srcset( $image_name, $size_array, $image_meta, $attachment_id = 0 ) {
if ( empty( $image_meta['sizes'] ) ) {
return false;
}
// Extract the height and width from the intermediate or the full size.
$img_width = ( $image ) ? $image['width'] : $img_meta['width'];
$img_height = ( $image ) ? $image['height'] : $img_meta['height'];
$image_sizes = $image_meta['sizes'];
// Bail early if the width isn't greater than zero.
if ( ! $img_width > 0 ) {
// Get the height and width for the image.
$image_width = (int) $size_array[0];
$image_height = (int) $size_array[1];
// Bail early if error/no width.
if ( $image_width < 1 ) {
return false;
}
// Use the URL from the intermediate size or build the url from the metadata.
if ( ! empty( $image['url'] ) ) {
$img_url = $image['url'];
} else {
$uploads_dir = wp_upload_dir();
$img_file = ( $image ) ? path_join( dirname( $img_meta['file'] ) , $image['file'] ) : $img_meta['file'];
$img_url = $uploads_dir['baseurl'] . '/' . $img_file;
}
$img_sizes = $img_meta['sizes'];
// Add full size to the img_sizes array.
$img_sizes['full'] = array(
'width' => $img_meta['width'],
'height' => $img_meta['height'],
'file' => wp_basename( $img_meta['file'] )
$image_sizes['full'] = array(
'width' => $image_meta['width'],
'height' => $image_meta['height'],
'file' => wp_basename( $image_meta['file'] ),
);
$image_baseurl = _wp_upload_dir_baseurl();
$dirname = dirname( $image_meta['file'] );
if ( $dirname !== '.' ) {
$image_baseurl = path_join( $image_baseurl, $dirname );
}
// Calculate the image aspect ratio.
$img_ratio = $img_height / $img_width;
$image_ratio = $image_height / $image_width;
/*
* Images that have been edited in WordPress after being uploaded will
* contain a unique hash. Look for that hash and use it later to filter
* out images that are leftovers from previous versions.
*/
$img_edited = preg_match( '/-e[0-9]{13}/', $img_url, $img_edit_hash );
$image_edited = preg_match( '/-e[0-9]{13}/', $image_name, $image_edit_hash );
/**
* Filter the maximum width included in a srcset attribute.
* Filter the maximum width included in a 'srcset' attribute.
*
* @since 4.4.0
*
* @param array|string $size Size of image, either array or string.
* @param int $max_width The maximum width to include in the 'srcset'. Default '1600'.
* @param array|string $size_array Array of width and height values in pixels (in that order).
*/
$max_srcset_width = apply_filters( 'max_srcset_image_width', 1600, $size );
$max_srcset_width = apply_filters( 'max_srcset_image_width', 1600, $size_array );
// Array to hold URL candidates.
$sources = array();
/*
* Set up arrays to hold url candidates and matched image sources so
* we can avoid duplicates without looping through the full sources array
* Loop through available images. Only use images that are resized
* versions of the same edit.
*/
$candidates = $sources = array();
foreach ( $image_sizes as $image ) {
/*
* Loop through available images and only use images that are resized
* versions of the same rendition.
*/
foreach ( $img_sizes as $img ) {
// Filter out images that are leftovers from previous renditions.
if ( $img_edited && ! strpos( $img['file'], $img_edit_hash[0] ) ) {
// Filter out images that are from previous edits.
if ( $image_edited && ! strpos( $image['file'], $image_edit_hash[0] ) ) {
continue;
}
// Filter out images that are wider than $max_srcset_width.
if ( $max_srcset_width && $img['width'] > $max_srcset_width ) {
if ( $max_srcset_width && $image['width'] > $max_srcset_width ) {
continue;
}
$candidate_url = path_join( dirname( $img_url ), $img['file'] );
$candidate_url = $image['file'];
// Calculate the new image ratio.
if ( $img['width'] ) {
$img_ratio_compare = $img['height'] / $img['width'];
if ( $image['width'] ) {
$image_ratio_compare = $image['height'] / $image['width'];
} else {
$img_ratio_compare = 0;
$image_ratio_compare = 0;
}
// If the new ratio differs by less than 0.01, use it.
if ( abs( $img_ratio - $img_ratio_compare ) < 0.01 && ! in_array( $candidate_url, $candidates ) ) {
// Add the URL to our list of candidates.
$candidates[] = $candidate_url;
// Add the url, descriptor, and value to the sources array to be returned.
$sources[] = array(
'url' => $candidate_url,
if ( abs( $image_ratio - $image_ratio_compare ) < 0.01 && ! array_key_exists( $candidate_url, $sources ) ) {
// Add the URL, descriptor, and value to the sources array to be returned.
$sources[ $image['width'] ] = array(
'url' => path_join( $image_baseurl, $candidate_url ),
'descriptor' => 'w',
'value' => $img['width'],
'value' => $image['width'],
);
}
}
@ -992,81 +1084,55 @@ function wp_get_attachment_image_srcset_array( $attachment_id, $size = 'medium'
*
* @since 4.4.0
*
* @param array $sources An array of image urls and widths.
* @param array $sources An array of image URLs and widths.
* @param int $attachment_id Attachment ID for image.
* @param array|string $size Image size. Accepts any valid image size, or an array of width and height
* values in pixels (in that order). Default 'medium'.
* @param array $image_meta The image meta data.
*/
return apply_filters( 'wp_get_attachment_image_srcset_array', $sources, $attachment_id, $size );
return apply_filters( 'wp_get_attachment_image_srcset_array', array_values( $sources ), $attachment_id, $size_array, $image_meta );
}
/**
* Retrieves the value for an image attachment's 'srcset' attribute.
* Create `sizes` attribute value for an image.
*
* @since 4.4.0
*
* @param int $attachment_id Image attachment ID.
* @param array|string $size Image size. Accepts any valid image size, or an array of width and height
* values in pixels (in that order). Default 'medium'.
* @return string|bool A 'srcset' value string or false.
*/
function wp_get_attachment_image_srcset( $attachment_id, $size = 'medium' ) {
$srcset_array = wp_get_attachment_image_srcset_array( $attachment_id, $size );
// Only return a srcset value if there is more than one source.
if ( count( $srcset_array ) <= 1 ) {
return false;
}
$srcset = '';
foreach ( $srcset_array as $source ) {
$srcset .= $source['url'] . ' ' . $source['value'] . $source['descriptor'] . ', ';
}
/**
* Filter the output of wp_get_attachment_image_srcset().
*
* @since 4.4.0
*
* @param string $srcset A source set formated for a `srcset` attribute.
* @param int $attachment_id Attachment ID for image.
* @param array|string $size Image size. Accepts any valid image size, or an array of width and height
* values in pixels (in that order). Default 'medium'.
*/
return apply_filters( 'wp_get_attachment_image_srcset', rtrim( $srcset, ', ' ), $attachment_id, $size );
}
/**
* Retrieves a source size attribute for an image from an array of values.
* @param array|string $size Image size. Accepts any valid image size name (thumbnail, medium, etc.),
* or an array of width and height values in pixels (in that order).
* @param array $image_meta Optional. The image meta data.
* @param int $attachment_id Optional. Image attachment ID. Either $image_meta or $attachment_id is needed
* when using image size name.
*
* @since 4.4.0
*
* @param int $attachment_id Image attachment ID.
* @param array|string $size Image size. Accepts any valid image size, or an array of width and height
* values in pixels (in that order). Default 'medium'.
* @param int $width Optional. Display width of the image.
* @return string|bool A valid source size value for use in a 'sizes' attribute or false.
*/
function wp_get_attachment_image_sizes( $attachment_id, $size = 'medium', $width = null ) {
// Try to get the image width from the $width parameter.
if ( is_numeric( $width ) ) {
$img_width = (int) $width;
// Next, see if a width value was passed in the $size parameter.
function wp_get_attachment_image_sizes( $size, $image_meta = null, $attachment_id = 0 ) {
$width = 0;
if ( is_numeric( $size ) ) {
$width = absint( $size );
} elseif ( is_array( $size ) ) {
$img_width = $size[0];
// Finally, use the $size name to return the width of the image.
} else {
$image = image_get_intermediate_size( $attachment_id, $size );
$img_width = $image ? $image['width'] : false;
$width = absint( $size[0] );
} elseif ( is_string( $size ) ) {
if ( ! $image_meta && $attachment_id ) {
$image_meta = wp_get_attachment_metadata( $attachment_id );
}
if ( $image_meta ) {
$width = _wp_get_image_size_from_meta( $size, $image_meta );
if ( $width ) {
$width = $width[0];
}
}
}
// Bail early if $img_width isn't set.
if ( ! $img_width ) {
if ( ! $width ) {
return false;
}
// Setup the default sizes attribute.
$sizes = sprintf( '(max-width: %1$dpx) 100vw, %1$dpx', $img_width );
$sizes = sprintf( '(max-width: %1$dpx) 100vw, %1$dpx', (int) $width );
/**
* Filter the output of wp_get_attachment_image_sizes().
@ -1074,12 +1140,12 @@ function wp_get_attachment_image_sizes( $attachment_id, $size = 'medium', $width
* @since 4.4.0
*
* @param string $sizes A source size value for use in a 'sizes' attribute.
* @param int $attachment_id Post ID of the original image.
* @param array|string $size Image size. Accepts any valid image size, or an array of width and height
* values in pixels (in that order). Default 'medium'.
* @param int $width Display width of the image.
* @param array $image_meta The image meta data.
* @param int $attachment_id Post ID of the original image.
*/
return apply_filters( 'wp_get_attachment_image_sizes', $sizes, $attachment_id, $size, $width );
return apply_filters( 'wp_get_attachment_image_sizes', $sizes, $size, $image_meta, $attachment_id );
}
/**
@ -1087,7 +1153,7 @@ function wp_get_attachment_image_sizes( $attachment_id, $size = 'medium', $width
*
* @since 4.4.0
*
* @see wp_img_add_srcset_and_sizes()
* @see wp_image_add_srcset_and_sizes()
*
* @param string $content The raw post content to be filtered.
* @return string Converted content with 'srcset' and 'sizes' attributes added to images.
@ -1095,29 +1161,33 @@ function wp_get_attachment_image_sizes( $attachment_id, $size = 'medium', $width
function wp_make_content_images_responsive( $content ) {
$images = get_media_embedded_in_content( $content, 'img' );
$attachment_ids = array();
$selected_images = $attachment_ids = array();
foreach( $images as $image ) {
if ( preg_match( '/wp-image-([0-9]+)/i', $image, $class_id ) ) {
$attachment_id = (int) $class_id[1];
if ( $attachment_id ) {
$attachment_ids[] = $attachment_id;
}
if ( false === strpos( $image, ' srcset="' ) && preg_match( '/wp-image-([0-9]+)/i', $image, $class_id ) &&
( $attachment_id = absint( $class_id[1] ) ) ) {
// If exactly the same image tag is used more than once, overwrite it.
// All identical tags will be replaced later with str_replace().
$selected_images[ $image ] = $attachment_id;
// Overwrite the ID when the same image is included more than once.
$attachment_ids[ $attachment_id ] = true;
}
}
if ( 0 < count( $attachment_ids ) ) {
if ( count( $attachment_ids ) > 1 ) {
/*
* Warm object caches for use with wp_get_attachment_metadata.
* Warm object cache for use with get_post_meta().
*
* To avoid making a database call for each image, a single query
* warms the object cache with the meta information for all images.
*/
_prime_post_caches( $attachment_ids, false, true );
update_meta_cache( 'post', array_keys( $attachment_ids ) );
}
foreach( $images as $image ) {
$content = str_replace( $image, wp_img_add_srcset_and_sizes( $image ), $content );
foreach ( $selected_images as $image => $attachment_id ) {
$image_meta = get_post_meta( $attachment_id, '_wp_attachment_metadata', true );
$content = str_replace( $image, wp_image_add_srcset_and_sizes( $image, $image_meta, $attachment_id ), $content );
}
return $content;
@ -1131,74 +1201,76 @@ function wp_make_content_images_responsive( $content ) {
* @see wp_get_attachment_image_srcset()
* @see wp_get_attachment_image_sizes()
*
* @param string $image An HTML 'img' element to be filtered.
* @param string $image An HTML 'img' element to be filtered.
* @param array $image_meta The image meta data.
* @param int $attachment_id Image attachment ID.
* @return string Converted 'img' element with `srcset` and `sizes` attributes added.
*/
function wp_img_add_srcset_and_sizes( $image ) {
// Return early if a 'srcset' attribute already exists.
if ( false !== strpos( $image, ' srcset="' ) ) {
function wp_image_add_srcset_and_sizes( $image, $image_meta, $attachment_id ) {
// Ensure the image meta exists
if ( empty( $image_meta['sizes'] ) ) {
return $image;
}
// Parse id, size, width, and height from the `img` element.
$id = preg_match( '/wp-image-([0-9]+)/i', $image, $match_id ) ? (int) $match_id[1] : false;
$size = preg_match( '/size-([^\s|"]+)/i', $image, $match_size ) ? $match_size[1] : false;
$width = preg_match( '/ width="([0-9]+)"/', $image, $match_width ) ? (int) $match_width[1] : false;
$src = preg_match( '/src="([^"]+)"/', $image, $match_src ) ? $match_src[1] : '';
list( $src ) = explode( '?', $src );
if ( $id && ! $size ) {
$height = preg_match( '/ height="([0-9]+)"/', $image, $match_height ) ? (int) $match_height[1] : false;
if ( $width && $height ) {
$size = array( $width, $height );
}
// Return early if we coudn't get the image source.
if ( ! $src ) {
return $image;
}
/*
* If attempts to parse the size value failed, attempt to use the image
* metadata to match the 'src' against the available sizes for an attachment.
*/
if ( $id && ! $size ) {
$meta = wp_get_attachment_metadata( $id );
// Bail early when an image has been inserted and later edited.
if ( preg_match( '/-e[0-9]{13}/', $image_meta['file'], $img_edit_hash ) &&
strpos( wp_basename( $src ), $img_edit_hash[0] ) === false ) {
// Parse the image src value from the img element.
$src = preg_match( '/src="([^"]+)"/', $image, $match_src ) ? $match_src[1] : false;
return $image;
}
// Return early if the metadata does not exist or the src value is empty.
if ( ! $meta || ! $src ) {
return $image;
}
$width = preg_match( '/ width="([0-9]+)"/', $image, $match_width ) ? (int) $match_width[1] : 0;
$height = preg_match( '/ height="([0-9]+)"/', $image, $match_height ) ? (int) $match_height[1] : 0;
if ( ! $width || ! $height ) {
/*
* First, see if the file is the full size image. If not, loop through
* the intermediate sizes until we find a file that matches.
* If attempts to parse the size value failed, attempt to use the image
* metadata to match the image file name from 'src' against the available sizes for an attachment.
*/
$image_filename = wp_basename( $src );
if ( $image_filename === basename( $meta['file'] ) ) {
$size = 'full';
if ( $image_filename === wp_basename( $image_meta['file'] ) ) {
$width = (int) $image_meta['width'];
$height = (int) $image_meta['height'];
} else {
foreach( $meta['sizes'] as $image_size => $image_size_data ) {
foreach( $image_meta['sizes'] as $image_size_data ) {
if ( $image_filename === $image_size_data['file'] ) {
$size = $image_size;
$width = (int) $image_size_data['width'];
$height = (int) $image_size_data['height'];
break;
}
}
}
}
// If ID and size exist, try for 'srcset' and 'sizes' and update the markup.
if ( $id && $size ) {
$srcset = wp_get_attachment_image_srcset( $id, $size );
$sizes = wp_get_attachment_image_sizes( $id, $size, $width );
if ( ! $width || ! $height ) {
return $image;
}
if ( $srcset && $sizes ) {
// Format the srcset and sizes string and escape attributes.
$srcset_and_sizes = sprintf( ' srcset="%s" sizes="%s"', esc_attr( $srcset ), esc_attr( $sizes) );
$size_array = array( $width, $height );
$sources = wp_calculate_image_srcset( $src, $size_array, $image_meta, $attachment_id );
// Add srcset and sizes attributes to the image markup.
$image = preg_replace( '/<img ([^>]+)[\s?][\/?]>/', '<img $1' . $srcset_and_sizes . ' />', $image );
}
$srcset = $sizes = '';
// Only calculate srcset and sizes values if there is more than one source.
if ( count( $sources ) > 1 ) {
$srcset = wp_image_srcset_attr( $sources, $size_array, $image_meta, $attachment_id );
$sizes = wp_get_attachment_image_sizes( $size_array, $image_meta, $attachment_id );
}
if ( $srcset && $sizes ) {
// Format the srcset and sizes string and escape attributes.
$srcset_and_sizes = sprintf( ' srcset="%s" sizes="%s"', esc_attr( $srcset ), esc_attr( $sizes ) );
// Add srcset and sizes attributes to the image markup.
$image = preg_replace( '/<img ([^>]+?)[\/ ]*>/', '<img $1' . $srcset_and_sizes . ' />', $image );
}
return $image;

View File

@ -4,7 +4,7 @@
*
* @global string $wp_version
*/
$wp_version = '4.4-beta1-35411';
$wp_version = '4.4-beta1-35412';
/**
* Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.