From a415c29efee45520ae252d2aa28f1083a521cd7b Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 17 Apr 2024 09:56:49 +0200 Subject: Adding upstream version 6.4.3+dfsg1. Signed-off-by: Daniel Baumann --- wp-includes/media.php | 5977 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 5977 insertions(+) create mode 100644 wp-includes/media.php (limited to 'wp-includes/media.php') diff --git a/wp-includes/media.php b/wp-includes/media.php new file mode 100644 index 0000000..078e3a2 --- /dev/null +++ b/wp-includes/media.php @@ -0,0 +1,5977 @@ + 0 ) { + $max_width = min( (int) $content_width, $max_width ); + } + } elseif ( 'large' === $size ) { + /* + * We're inserting a large size image into the editor. If it's a really + * big image we'll scale it down to fit reasonably within the editor + * itself, and within the theme's content width if it's known. The user + * can resize it in the editor if they wish. + */ + $max_width = (int) get_option( 'large_size_w' ); + $max_height = (int) get_option( 'large_size_h' ); + + if ( (int) $content_width > 0 ) { + $max_width = min( (int) $content_width, $max_width ); + } + } elseif ( ! empty( $_wp_additional_image_sizes ) && in_array( $size, array_keys( $_wp_additional_image_sizes ), true ) ) { + $max_width = (int) $_wp_additional_image_sizes[ $size ]['width']; + $max_height = (int) $_wp_additional_image_sizes[ $size ]['height']; + // Only in admin. Assume that theme authors know what they're doing. + if ( (int) $content_width > 0 && 'edit' === $context ) { + $max_width = min( (int) $content_width, $max_width ); + } + } else { // $size === 'full' has no constraint. + $max_width = $width; + $max_height = $height; + } + + /** + * Filters the maximum image size dimensions for the editor. + * + * @since 2.5.0 + * + * @param int[] $max_image_size { + * An array of width and height values. + * + * @type int $0 The maximum width in pixels. + * @type int $1 The maximum height in pixels. + * } + * @param string|int[] $size Requested image size. Can be any registered image size name, or + * an array of width and height values in pixels (in that order). + * @param string $context The context the image is being resized for. + * Possible values are 'display' (like in a theme) + * or 'edit' (like inserting into an editor). + */ + list( $max_width, $max_height ) = apply_filters( 'editor_max_image_size', array( $max_width, $max_height ), $size, $context ); + + return wp_constrain_dimensions( $width, $height, $max_width, $max_height ); +} + +/** + * Retrieves width and height attributes using given width and height values. + * + * Both attributes are required in the sense that both parameters must have a + * value, but are optional in that if you set them to false or null, then they + * will not be added to the returned string. + * + * You can set the value using a string, but it will only take numeric values. + * If you wish to put 'px' after the numbers, then it will be stripped out of + * the return. + * + * @since 2.5.0 + * + * @param int|string $width Image width in pixels. + * @param int|string $height Image height in pixels. + * @return string HTML attributes for width and, or height. + */ +function image_hwstring( $width, $height ) { + $out = ''; + if ( $width ) { + $out .= 'width="' . (int) $width . '" '; + } + if ( $height ) { + $out .= 'height="' . (int) $height . '" '; + } + return $out; +} + +/** + * Scales an image to fit a particular size (such as 'thumb' or 'medium'). + * + * The URL might be the original image, or it might be a resized version. This + * function won't create a new resized copy, it will just return an already + * resized one if it exists. + * + * A plugin may use the {@see 'image_downsize'} filter to hook into and offer image + * resizing services for images. The hook must return an array with the same + * elements that are normally returned from the function. + * + * @since 2.5.0 + * + * @param int $id Attachment ID for image. + * @param string|int[] $size Optional. Image size. Accepts any registered image size name, or an array + * of width and height values in pixels (in that order). Default 'medium'. + * @return array|false { + * Array of image data, or boolean false if no image is available. + * + * @type string $0 Image source URL. + * @type int $1 Image width in pixels. + * @type int $2 Image height in pixels. + * @type bool $3 Whether the image is a resized image. + * } + */ +function image_downsize( $id, $size = 'medium' ) { + $is_image = wp_attachment_is_image( $id ); + + /** + * Filters whether to preempt the output of image_downsize(). + * + * Returning a truthy value from the filter will effectively short-circuit + * down-sizing the image, returning that value instead. + * + * @since 2.5.0 + * + * @param bool|array $downsize Whether to short-circuit the image downsize. + * @param int $id Attachment ID for image. + * @param string|int[] $size Requested image size. Can be any registered image size name, or + * an array of width and height values in pixels (in that order). + */ + $out = apply_filters( 'image_downsize', false, $id, $size ); + + if ( $out ) { + return $out; + } + + $img_url = wp_get_attachment_url( $id ); + $meta = wp_get_attachment_metadata( $id ); + $width = 0; + $height = 0; + $is_intermediate = false; + $img_url_basename = wp_basename( $img_url ); + + /* + * If the file isn't an image, attempt to replace its URL with a rendered image from its meta. + * Otherwise, a non-image type could be returned. + */ + if ( ! $is_image ) { + if ( ! empty( $meta['sizes']['full'] ) ) { + $img_url = str_replace( $img_url_basename, $meta['sizes']['full']['file'], $img_url ); + $img_url_basename = $meta['sizes']['full']['file']; + $width = $meta['sizes']['full']['width']; + $height = $meta['sizes']['full']['height']; + } else { + return false; + } + } + + // Try for a new style intermediate size. + $intermediate = image_get_intermediate_size( $id, $size ); + + if ( $intermediate ) { + $img_url = str_replace( $img_url_basename, $intermediate['file'], $img_url ); + $width = $intermediate['width']; + $height = $intermediate['height']; + $is_intermediate = true; + } elseif ( 'thumbnail' === $size && ! empty( $meta['thumb'] ) && is_string( $meta['thumb'] ) ) { + // Fall back to the old thumbnail. + $imagefile = get_attached_file( $id ); + $thumbfile = str_replace( wp_basename( $imagefile ), wp_basename( $meta['thumb'] ), $imagefile ); + + if ( file_exists( $thumbfile ) ) { + $info = wp_getimagesize( $thumbfile ); + + if ( $info ) { + $img_url = str_replace( $img_url_basename, wp_basename( $thumbfile ), $img_url ); + $width = $info[0]; + $height = $info[1]; + $is_intermediate = true; + } + } + } + + if ( ! $width && ! $height && isset( $meta['width'], $meta['height'] ) ) { + // Any other type: use the real image. + $width = $meta['width']; + $height = $meta['height']; + } + + if ( $img_url ) { + // We have the actual image size, but might need to further constrain it if content_width is narrower. + list( $width, $height ) = image_constrain_size_for_editor( $width, $height, $size ); + + return array( $img_url, $width, $height, $is_intermediate ); + } + + return false; +} + +/** + * Registers a new image size. + * + * @since 2.9.0 + * + * @global array $_wp_additional_image_sizes Associative array of additional image sizes. + * + * @param string $name Image size identifier. + * @param int $width Optional. Image width in pixels. Default 0. + * @param int $height Optional. Image height in pixels. Default 0. + * @param bool|array $crop { + * Optional. Image cropping behavior. If false, the image will be scaled (default). + * If true, image will be cropped to the specified dimensions using center positions. + * If an array, the image will be cropped using the array to specify the crop location: + * + * @type string $0 The x crop position. Accepts 'left' 'center', or 'right'. + * @type string $1 The y crop position. Accepts 'top', 'center', or 'bottom'. + * } + */ +function add_image_size( $name, $width = 0, $height = 0, $crop = false ) { + global $_wp_additional_image_sizes; + + $_wp_additional_image_sizes[ $name ] = array( + 'width' => absint( $width ), + 'height' => absint( $height ), + 'crop' => $crop, + ); +} + +/** + * Checks if an image size exists. + * + * @since 3.9.0 + * + * @param string $name The image size to check. + * @return bool True if the image size exists, false if not. + */ +function has_image_size( $name ) { + $sizes = wp_get_additional_image_sizes(); + return isset( $sizes[ $name ] ); +} + +/** + * Removes a new image size. + * + * @since 3.9.0 + * + * @global array $_wp_additional_image_sizes + * + * @param string $name The image size to remove. + * @return bool True if the image size was successfully removed, false on failure. + */ +function remove_image_size( $name ) { + global $_wp_additional_image_sizes; + + if ( isset( $_wp_additional_image_sizes[ $name ] ) ) { + unset( $_wp_additional_image_sizes[ $name ] ); + return true; + } + + return false; +} + +/** + * Registers an image size for the post thumbnail. + * + * @since 2.9.0 + * + * @see add_image_size() for details on cropping behavior. + * + * @param int $width Image width in pixels. + * @param int $height Image height in pixels. + * @param bool|array $crop { + * Optional. Image cropping behavior. If false, the image will be scaled (default). + * If true, image will be cropped to the specified dimensions using center positions. + * If an array, the image will be cropped using the array to specify the crop location: + * + * @type string $0 The x crop position. Accepts 'left' 'center', or 'right'. + * @type string $1 The y crop position. Accepts 'top', 'center', or 'bottom'. + * } + */ +function set_post_thumbnail_size( $width = 0, $height = 0, $crop = false ) { + add_image_size( 'post-thumbnail', $width, $height, $crop ); +} + +/** + * Gets an img tag for an image attachment, scaling it down if requested. + * + * The {@see 'get_image_tag_class'} filter allows for changing the class name for the + * image without having to use regular expressions on the HTML content. The + * parameters are: what WordPress will use for the class, the Attachment ID, + * image align value, and the size the image should be. + * + * The second filter, {@see 'get_image_tag'}, has the HTML content, which can then be + * further manipulated by a plugin to change all attribute values and even HTML + * content. + * + * @since 2.5.0 + * + * @param int $id Attachment ID. + * @param string $alt Image description for the alt attribute. + * @param string $title Image description for the title attribute. + * @param string $align Part of the class name for aligning the image. + * @param string|int[] $size Optional. Image size. Accepts any registered image size name, or an array of + * width and height values in pixels (in that order). Default 'medium'. + * @return string HTML IMG element for given image attachment? + */ +function get_image_tag( $id, $alt, $title, $align, $size = 'medium' ) { + + list( $img_src, $width, $height ) = image_downsize( $id, $size ); + $hwstring = image_hwstring( $width, $height ); + + $title = $title ? 'title="' . esc_attr( $title ) . '" ' : ''; + + $size_class = is_array( $size ) ? implode( 'x', $size ) : $size; + $class = 'align' . esc_attr( $align ) . ' size-' . esc_attr( $size_class ) . ' wp-image-' . $id; + + /** + * Filters the value of the attachment's image tag class attribute. + * + * @since 2.6.0 + * + * @param string $class CSS class name or space-separated list of classes. + * @param int $id Attachment ID. + * @param string $align Part of the class name for aligning the image. + * @param string|int[] $size Requested image size. Can be any registered image size name, or + * an array of width and height values in pixels (in that order). + */ + $class = apply_filters( 'get_image_tag_class', $class, $id, $align, $size ); + + $html = '' . esc_attr( $alt ) . ''; + + /** + * Filters the HTML content for the image tag. + * + * @since 2.6.0 + * + * @param string $html HTML content for the image. + * @param int $id Attachment ID. + * @param string $alt Image description for the alt attribute. + * @param string $title Image description for the title attribute. + * @param string $align Part of the class name for aligning the image. + * @param string|int[] $size Requested image size. Can be any registered image size name, or + * an array of width and height values in pixels (in that order). + */ + return apply_filters( 'get_image_tag', $html, $id, $alt, $title, $align, $size ); +} + +/** + * Calculates the new dimensions for a down-sampled image. + * + * If either width or height are empty, no constraint is applied on + * that dimension. + * + * @since 2.5.0 + * + * @param int $current_width Current width of the image. + * @param int $current_height Current height of the image. + * @param int $max_width Optional. Max width in pixels to constrain to. Default 0. + * @param int $max_height Optional. Max height in pixels to constrain to. Default 0. + * @return int[] { + * An array of width and height values. + * + * @type int $0 The width in pixels. + * @type int $1 The height in pixels. + * } + */ +function wp_constrain_dimensions( $current_width, $current_height, $max_width = 0, $max_height = 0 ) { + if ( ! $max_width && ! $max_height ) { + return array( $current_width, $current_height ); + } + + $width_ratio = 1.0; + $height_ratio = 1.0; + $did_width = false; + $did_height = false; + + if ( $max_width > 0 && $current_width > 0 && $current_width > $max_width ) { + $width_ratio = $max_width / $current_width; + $did_width = true; + } + + if ( $max_height > 0 && $current_height > 0 && $current_height > $max_height ) { + $height_ratio = $max_height / $current_height; + $did_height = true; + } + + // Calculate the larger/smaller ratios. + $smaller_ratio = min( $width_ratio, $height_ratio ); + $larger_ratio = max( $width_ratio, $height_ratio ); + + if ( (int) round( $current_width * $larger_ratio ) > $max_width || (int) round( $current_height * $larger_ratio ) > $max_height ) { + // The larger ratio is too big. It would result in an overflow. + $ratio = $smaller_ratio; + } else { + // The larger ratio fits, and is likely to be a more "snug" fit. + $ratio = $larger_ratio; + } + + // Very small dimensions may result in 0, 1 should be the minimum. + $w = max( 1, (int) round( $current_width * $ratio ) ); + $h = max( 1, (int) round( $current_height * $ratio ) ); + + /* + * Sometimes, due to rounding, we'll end up with a result like this: + * 465x700 in a 177x177 box is 117x176... a pixel short. + * We also have issues with recursive calls resulting in an ever-changing result. + * Constraining to the result of a constraint should yield the original result. + * Thus we look for dimensions that are one pixel shy of the max value and bump them up. + */ + + // Note: $did_width means it is possible $smaller_ratio == $width_ratio. + if ( $did_width && $w === $max_width - 1 ) { + $w = $max_width; // Round it up. + } + + // Note: $did_height means it is possible $smaller_ratio == $height_ratio. + if ( $did_height && $h === $max_height - 1 ) { + $h = $max_height; // Round it up. + } + + /** + * Filters dimensions to constrain down-sampled images to. + * + * @since 4.1.0 + * + * @param int[] $dimensions { + * An array of width and height values. + * + * @type int $0 The width in pixels. + * @type int $1 The height in pixels. + * } + * @param int $current_width The current width of the image. + * @param int $current_height The current height of the image. + * @param int $max_width The maximum width permitted. + * @param int $max_height The maximum height permitted. + */ + return apply_filters( 'wp_constrain_dimensions', array( $w, $h ), $current_width, $current_height, $max_width, $max_height ); +} + +/** + * Retrieves calculated resize dimensions for use in WP_Image_Editor. + * + * Calculates dimensions and coordinates for a resized image that fits + * within a specified width and height. + * + * @since 2.5.0 + * + * @param int $orig_w Original width in pixels. + * @param int $orig_h Original height in pixels. + * @param int $dest_w New width in pixels. + * @param int $dest_h New height in pixels. + * @param bool|array $crop { + * Optional. Image cropping behavior. If false, the image will be scaled (default). + * If true, image will be cropped to the specified dimensions using center positions. + * If an array, the image will be cropped using the array to specify the crop location: + * + * @type string $0 The x crop position. Accepts 'left' 'center', or 'right'. + * @type string $1 The y crop position. Accepts 'top', 'center', or 'bottom'. + * } + * @return array|false Returned array matches parameters for `imagecopyresampled()`. False on failure. + */ +function image_resize_dimensions( $orig_w, $orig_h, $dest_w, $dest_h, $crop = false ) { + + if ( $orig_w <= 0 || $orig_h <= 0 ) { + return false; + } + // At least one of $dest_w or $dest_h must be specific. + if ( $dest_w <= 0 && $dest_h <= 0 ) { + return false; + } + + /** + * Filters whether to preempt calculating the image resize dimensions. + * + * Returning a non-null value from the filter will effectively short-circuit + * image_resize_dimensions(), returning that value instead. + * + * @since 3.4.0 + * + * @param null|mixed $null Whether to preempt output of the resize dimensions. + * @param int $orig_w Original width in pixels. + * @param int $orig_h Original height in pixels. + * @param int $dest_w New width in pixels. + * @param int $dest_h New height in pixels. + * @param bool|array $crop Whether to crop image to specified width and height or resize. + * An array can specify positioning of the crop area. Default false. + */ + $output = apply_filters( 'image_resize_dimensions', null, $orig_w, $orig_h, $dest_w, $dest_h, $crop ); + + if ( null !== $output ) { + return $output; + } + + // Stop if the destination size is larger than the original image dimensions. + if ( empty( $dest_h ) ) { + if ( $orig_w < $dest_w ) { + return false; + } + } elseif ( empty( $dest_w ) ) { + if ( $orig_h < $dest_h ) { + return false; + } + } else { + if ( $orig_w < $dest_w && $orig_h < $dest_h ) { + return false; + } + } + + if ( $crop ) { + /* + * Crop the largest possible portion of the original image that we can size to $dest_w x $dest_h. + * Note that the requested crop dimensions are used as a maximum bounding box for the original image. + * If the original image's width or height is less than the requested width or height + * only the greater one will be cropped. + * For example when the original image is 600x300, and the requested crop dimensions are 400x400, + * the resulting image will be 400x300. + */ + $aspect_ratio = $orig_w / $orig_h; + $new_w = min( $dest_w, $orig_w ); + $new_h = min( $dest_h, $orig_h ); + + if ( ! $new_w ) { + $new_w = (int) round( $new_h * $aspect_ratio ); + } + + if ( ! $new_h ) { + $new_h = (int) round( $new_w / $aspect_ratio ); + } + + $size_ratio = max( $new_w / $orig_w, $new_h / $orig_h ); + + $crop_w = round( $new_w / $size_ratio ); + $crop_h = round( $new_h / $size_ratio ); + + if ( ! is_array( $crop ) || count( $crop ) !== 2 ) { + $crop = array( 'center', 'center' ); + } + + list( $x, $y ) = $crop; + + if ( 'left' === $x ) { + $s_x = 0; + } elseif ( 'right' === $x ) { + $s_x = $orig_w - $crop_w; + } else { + $s_x = floor( ( $orig_w - $crop_w ) / 2 ); + } + + if ( 'top' === $y ) { + $s_y = 0; + } elseif ( 'bottom' === $y ) { + $s_y = $orig_h - $crop_h; + } else { + $s_y = floor( ( $orig_h - $crop_h ) / 2 ); + } + } else { + // Resize using $dest_w x $dest_h as a maximum bounding box. + $crop_w = $orig_w; + $crop_h = $orig_h; + + $s_x = 0; + $s_y = 0; + + list( $new_w, $new_h ) = wp_constrain_dimensions( $orig_w, $orig_h, $dest_w, $dest_h ); + } + + if ( wp_fuzzy_number_match( $new_w, $orig_w ) && wp_fuzzy_number_match( $new_h, $orig_h ) ) { + // The new size has virtually the same dimensions as the original image. + + /** + * Filters whether to proceed with making an image sub-size with identical dimensions + * with the original/source image. Differences of 1px may be due to rounding and are ignored. + * + * @since 5.3.0 + * + * @param bool $proceed The filtered value. + * @param int $orig_w Original image width. + * @param int $orig_h Original image height. + */ + $proceed = (bool) apply_filters( 'wp_image_resize_identical_dimensions', false, $orig_w, $orig_h ); + + if ( ! $proceed ) { + return false; + } + } + + /* + * The return array matches the parameters to imagecopyresampled(). + * int dst_x, int dst_y, int src_x, int src_y, int dst_w, int dst_h, int src_w, int src_h + */ + return array( 0, 0, (int) $s_x, (int) $s_y, (int) $new_w, (int) $new_h, (int) $crop_w, (int) $crop_h ); +} + +/** + * Resizes an image to make a thumbnail or intermediate size. + * + * The returned array has the file size, the image width, and image height. The + * {@see 'image_make_intermediate_size'} filter can be used to hook in and change the + * values of the returned array. The only parameter is the resized file path. + * + * @since 2.5.0 + * + * @param string $file File path. + * @param int $width Image width. + * @param int $height Image height. + * @param bool|array $crop { + * Optional. Image cropping behavior. If false, the image will be scaled (default). + * If true, image will be cropped to the specified dimensions using center positions. + * If an array, the image will be cropped using the array to specify the crop location: + * + * @type string $0 The x crop position. Accepts 'left' 'center', or 'right'. + * @type string $1 The y crop position. Accepts 'top', 'center', or 'bottom'. + * } + * @return array|false Metadata array on success. False if no image was created. + */ +function image_make_intermediate_size( $file, $width, $height, $crop = false ) { + if ( $width || $height ) { + $editor = wp_get_image_editor( $file ); + + if ( is_wp_error( $editor ) || is_wp_error( $editor->resize( $width, $height, $crop ) ) ) { + return false; + } + + $resized_file = $editor->save(); + + if ( ! is_wp_error( $resized_file ) && $resized_file ) { + unset( $resized_file['path'] ); + return $resized_file; + } + } + return false; +} + +/** + * Helper function to test if aspect ratios for two images match. + * + * @since 4.6.0 + * + * @param int $source_width Width of the first image in pixels. + * @param int $source_height Height of the first image in pixels. + * @param int $target_width Width of the second image in pixels. + * @param int $target_height Height of the second image in pixels. + * @return bool True if aspect ratios match within 1px. False if not. + */ +function wp_image_matches_ratio( $source_width, $source_height, $target_width, $target_height ) { + /* + * To test for varying crops, we constrain the dimensions of the larger image + * to the dimensions of the smaller image and see if they match. + */ + if ( $source_width > $target_width ) { + $constrained_size = wp_constrain_dimensions( $source_width, $source_height, $target_width ); + $expected_size = array( $target_width, $target_height ); + } else { + $constrained_size = wp_constrain_dimensions( $target_width, $target_height, $source_width ); + $expected_size = array( $source_width, $source_height ); + } + + // If the image dimensions are within 1px of the expected size, we consider it a match. + $matched = ( wp_fuzzy_number_match( $constrained_size[0], $expected_size[0] ) && wp_fuzzy_number_match( $constrained_size[1], $expected_size[1] ) ); + + return $matched; +} + +/** + * Retrieves the image's intermediate size (resized) path, width, and height. + * + * The $size parameter can be an array with the width and height respectively. + * If the size matches the 'sizes' metadata array for width and height, then it + * will be used. If there is no direct match, then the nearest image size larger + * than the specified size will be used. If nothing is found, then the function + * will break out and return false. + * + * The metadata 'sizes' is used for compatible sizes that can be used for the + * parameter $size value. + * + * The url path will be given, when the $size parameter is a string. + * + * If you are passing an array for the $size, you should consider using + * add_image_size() so that a cropped version is generated. It's much more + * efficient than having to find the closest-sized image and then having the + * browser scale down the image. + * + * @since 2.5.0 + * + * @param int $post_id Attachment ID. + * @param string|int[] $size Optional. Image size. Accepts any registered image size name, or an array + * of width and height values in pixels (in that order). Default 'thumbnail'. + * @return array|false { + * Array of file relative path, width, and height on success. Additionally includes absolute + * path and URL if registered size is passed to `$size` parameter. False on failure. + * + * @type string $file Filename of image. + * @type int $width Width of image in pixels. + * @type int $height Height of image in pixels. + * @type string $path Path of image relative to uploads directory. + * @type string $url URL of image. + * } + */ +function image_get_intermediate_size( $post_id, $size = 'thumbnail' ) { + $imagedata = wp_get_attachment_metadata( $post_id ); + + if ( ! $size || ! is_array( $imagedata ) || empty( $imagedata['sizes'] ) ) { + return false; + } + + $data = array(); + + // Find the best match when '$size' is an array. + if ( is_array( $size ) ) { + $candidates = array(); + + if ( ! isset( $imagedata['file'] ) && isset( $imagedata['sizes']['full'] ) ) { + $imagedata['height'] = $imagedata['sizes']['full']['height']; + $imagedata['width'] = $imagedata['sizes']['full']['width']; + } + + foreach ( $imagedata['sizes'] as $_size => $data ) { + // If there's an exact match to an existing image size, short circuit. + if ( (int) $data['width'] === (int) $size[0] && (int) $data['height'] === (int) $size[1] ) { + $candidates[ $data['width'] * $data['height'] ] = $data; + break; + } + + // If it's not an exact match, consider larger sizes with the same aspect ratio. + if ( $data['width'] >= $size[0] && $data['height'] >= $size[1] ) { + // If '0' is passed to either size, we test ratios against the original file. + if ( 0 === $size[0] || 0 === $size[1] ) { + $same_ratio = wp_image_matches_ratio( $data['width'], $data['height'], $imagedata['width'], $imagedata['height'] ); + } else { + $same_ratio = wp_image_matches_ratio( $data['width'], $data['height'], $size[0], $size[1] ); + } + + if ( $same_ratio ) { + $candidates[ $data['width'] * $data['height'] ] = $data; + } + } + } + + if ( ! empty( $candidates ) ) { + // Sort the array by size if we have more than one candidate. + if ( 1 < count( $candidates ) ) { + ksort( $candidates ); + } + + $data = array_shift( $candidates ); + /* + * When the size requested is smaller than the thumbnail dimensions, we + * fall back to the thumbnail size to maintain backward compatibility with + * pre 4.6 versions of WordPress. + */ + } elseif ( ! empty( $imagedata['sizes']['thumbnail'] ) && $imagedata['sizes']['thumbnail']['width'] >= $size[0] && $imagedata['sizes']['thumbnail']['width'] >= $size[1] ) { + $data = $imagedata['sizes']['thumbnail']; + } else { + return false; + } + + // Constrain the width and height attributes to the requested values. + list( $data['width'], $data['height'] ) = image_constrain_size_for_editor( $data['width'], $data['height'], $size ); + + } elseif ( ! empty( $imagedata['sizes'][ $size ] ) ) { + $data = $imagedata['sizes'][ $size ]; + } + + // If we still don't have a match at this point, return false. + if ( empty( $data ) ) { + return false; + } + + // Include the full filesystem path of the intermediate file. + if ( empty( $data['path'] ) && ! empty( $data['file'] ) && ! empty( $imagedata['file'] ) ) { + $file_url = wp_get_attachment_url( $post_id ); + $data['path'] = path_join( dirname( $imagedata['file'] ), $data['file'] ); + $data['url'] = path_join( dirname( $file_url ), $data['file'] ); + } + + /** + * Filters the output of image_get_intermediate_size() + * + * @since 4.4.0 + * + * @see image_get_intermediate_size() + * + * @param array $data Array of file relative path, width, and height on success. May also include + * file absolute path and URL. + * @param int $post_id The ID of the image attachment. + * @param string|int[] $size Requested image size. Can be any registered image size name, or + * an array of width and height values in pixels (in that order). + */ + return apply_filters( 'image_get_intermediate_size', $data, $post_id, $size ); +} + +/** + * Gets the available intermediate image size names. + * + * @since 3.0.0 + * + * @return string[] An array of image size names. + */ +function get_intermediate_image_sizes() { + $default_sizes = array( 'thumbnail', 'medium', 'medium_large', 'large' ); + $additional_sizes = wp_get_additional_image_sizes(); + + if ( ! empty( $additional_sizes ) ) { + $default_sizes = array_merge( $default_sizes, array_keys( $additional_sizes ) ); + } + + /** + * Filters the list of intermediate image sizes. + * + * @since 2.5.0 + * + * @param string[] $default_sizes An array of intermediate image size names. Defaults + * are 'thumbnail', 'medium', 'medium_large', 'large'. + */ + return apply_filters( 'intermediate_image_sizes', $default_sizes ); +} + +/** + * Returns a normalized list of all currently registered image sub-sizes. + * + * @since 5.3.0 + * @uses wp_get_additional_image_sizes() + * @uses get_intermediate_image_sizes() + * + * @return array[] Associative array of arrays of image sub-size information, + * keyed by image size name. + */ +function wp_get_registered_image_subsizes() { + $additional_sizes = wp_get_additional_image_sizes(); + $all_sizes = array(); + + foreach ( get_intermediate_image_sizes() as $size_name ) { + $size_data = array( + 'width' => 0, + 'height' => 0, + 'crop' => false, + ); + + if ( isset( $additional_sizes[ $size_name ]['width'] ) ) { + // For sizes added by plugins and themes. + $size_data['width'] = (int) $additional_sizes[ $size_name ]['width']; + } else { + // For default sizes set in options. + $size_data['width'] = (int) get_option( "{$size_name}_size_w" ); + } + + if ( isset( $additional_sizes[ $size_name ]['height'] ) ) { + $size_data['height'] = (int) $additional_sizes[ $size_name ]['height']; + } else { + $size_data['height'] = (int) get_option( "{$size_name}_size_h" ); + } + + if ( empty( $size_data['width'] ) && empty( $size_data['height'] ) ) { + // This size isn't set. + continue; + } + + if ( isset( $additional_sizes[ $size_name ]['crop'] ) ) { + $size_data['crop'] = $additional_sizes[ $size_name ]['crop']; + } else { + $size_data['crop'] = get_option( "{$size_name}_crop" ); + } + + if ( ! is_array( $size_data['crop'] ) || empty( $size_data['crop'] ) ) { + $size_data['crop'] = (bool) $size_data['crop']; + } + + $all_sizes[ $size_name ] = $size_data; + } + + return $all_sizes; +} + +/** + * Retrieves an image to represent an attachment. + * + * @since 2.5.0 + * + * @param int $attachment_id Image attachment ID. + * @param string|int[] $size Optional. Image size. Accepts any registered image size name, or an array of + * width and height values in pixels (in that order). Default 'thumbnail'. + * @param bool $icon Optional. Whether the image should fall back to a mime type icon. Default false. + * @return array|false { + * Array of image data, or boolean false if no image is available. + * + * @type string $0 Image source URL. + * @type int $1 Image width in pixels. + * @type int $2 Image height in pixels. + * @type bool $3 Whether the image is a resized image. + * } + */ +function wp_get_attachment_image_src( $attachment_id, $size = 'thumbnail', $icon = false ) { + // Get a thumbnail or intermediate image if there is one. + $image = image_downsize( $attachment_id, $size ); + if ( ! $image ) { + $src = false; + + if ( $icon ) { + $src = wp_mime_type_icon( $attachment_id ); + + if ( $src ) { + /** This filter is documented in wp-includes/post.php */ + $icon_dir = apply_filters( 'icon_dir', ABSPATH . WPINC . '/images/media' ); + + $src_file = $icon_dir . '/' . wp_basename( $src ); + list( $width, $height ) = wp_getimagesize( $src_file ); + } + } + + if ( $src && $width && $height ) { + $image = array( $src, $width, $height, false ); + } + } + /** + * Filters the attachment image source result. + * + * @since 4.3.0 + * + * @param array|false $image { + * Array of image data, or boolean false if no image is available. + * + * @type string $0 Image source URL. + * @type int $1 Image width in pixels. + * @type int $2 Image height in pixels. + * @type bool $3 Whether the image is a resized image. + * } + * @param int $attachment_id Image attachment ID. + * @param string|int[] $size Requested image size. Can be any registered image size name, or + * an array of width and height values in pixels (in that order). + * @param bool $icon Whether the image should be treated as an icon. + */ + return apply_filters( 'wp_get_attachment_image_src', $image, $attachment_id, $size, $icon ); +} + +/** + * Gets an HTML img element representing an image attachment. + * + * While `$size` will accept an array, it is better to register a size with + * add_image_size() so that a cropped version is generated. It's much more + * efficient than having to find the closest-sized image and then having the + * browser scale down the image. + * + * @since 2.5.0 + * @since 4.4.0 The `$srcset` and `$sizes` attributes were added. + * @since 5.5.0 The `$loading` attribute was added. + * @since 6.1.0 The `$decoding` attribute was added. + * + * @param int $attachment_id Image attachment ID. + * @param string|int[] $size Optional. Image size. Accepts any registered image size name, or an array + * of width and height values in pixels (in that order). Default 'thumbnail'. + * @param bool $icon Optional. Whether the image should be treated as an icon. Default false. + * @param string|array $attr { + * Optional. Attributes for the image markup. + * + * @type string $src Image attachment URL. + * @type string $class CSS class name or space-separated list of classes. + * Default `attachment-$size_class size-$size_class`, + * where `$size_class` is the image size being requested. + * @type string $alt Image description for the alt attribute. + * @type string $srcset The 'srcset' attribute value. + * @type string $sizes The 'sizes' attribute value. + * @type string|false $loading The 'loading' attribute value. Passing a value of false + * will result in the attribute being omitted for the image. + * Defaults to 'lazy', depending on wp_lazy_loading_enabled(). + * @type string $decoding The 'decoding' attribute value. Possible values are + * 'async' (default), 'sync', or 'auto'. Passing false or an empty + * string will result in the attribute being omitted. + * } + * @return string HTML img element or empty string on failure. + */ +function wp_get_attachment_image( $attachment_id, $size = 'thumbnail', $icon = false, $attr = '' ) { + $html = ''; + $image = wp_get_attachment_image_src( $attachment_id, $size, $icon ); + + if ( $image ) { + list( $src, $width, $height ) = $image; + + $attachment = get_post( $attachment_id ); + $hwstring = image_hwstring( $width, $height ); + $size_class = $size; + + if ( is_array( $size_class ) ) { + $size_class = implode( 'x', $size_class ); + } + + $default_attr = array( + 'src' => $src, + 'class' => "attachment-$size_class size-$size_class", + 'alt' => trim( strip_tags( get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ) ) ), + ); + + /** + * Filters the context in which wp_get_attachment_image() is used. + * + * @since 6.3.0 + * + * @param string $context The context. Default 'wp_get_attachment_image'. + */ + $context = apply_filters( 'wp_get_attachment_image_context', 'wp_get_attachment_image' ); + $attr = wp_parse_args( $attr, $default_attr ); + + $loading_attr = $attr; + $loading_attr['width'] = $width; + $loading_attr['height'] = $height; + $loading_optimization_attr = wp_get_loading_optimization_attributes( + 'img', + $loading_attr, + $context + ); + + // Add loading optimization attributes if not available. + $attr = array_merge( $attr, $loading_optimization_attr ); + + // Omit the `decoding` attribute if the value is invalid according to the spec. + if ( empty( $attr['decoding'] ) || ! in_array( $attr['decoding'], array( 'async', 'sync', 'auto' ), true ) ) { + unset( $attr['decoding'] ); + } + + /* + * If the default value of `lazy` for the `loading` attribute is overridden + * to omit the attribute for this image, ensure it is not included. + */ + if ( isset( $attr['loading'] ) && ! $attr['loading'] ) { + unset( $attr['loading'] ); + } + + // If the `fetchpriority` attribute is overridden and set to false or an empty string. + if ( isset( $attr['fetchpriority'] ) && ! $attr['fetchpriority'] ) { + unset( $attr['fetchpriority'] ); + } + + // Generate 'srcset' and 'sizes' if not already present. + if ( empty( $attr['srcset'] ) ) { + $image_meta = wp_get_attachment_metadata( $attachment_id ); + + if ( is_array( $image_meta ) ) { + $size_array = array( absint( $width ), absint( $height ) ); + $srcset = wp_calculate_image_srcset( $size_array, $src, $image_meta, $attachment_id ); + $sizes = wp_calculate_image_sizes( $size_array, $src, $image_meta, $attachment_id ); + + if ( $srcset && ( $sizes || ! empty( $attr['sizes'] ) ) ) { + $attr['srcset'] = $srcset; + + if ( empty( $attr['sizes'] ) ) { + $attr['sizes'] = $sizes; + } + } + } + } + + /** + * Filters the list of attachment image attributes. + * + * @since 2.8.0 + * + * @param string[] $attr Array of attribute values for the image markup, keyed by attribute name. + * See wp_get_attachment_image(). + * @param WP_Post $attachment Image attachment post. + * @param string|int[] $size Requested image size. Can be any registered image size name, or + * an array of width and height values in pixels (in that order). + */ + $attr = apply_filters( 'wp_get_attachment_image_attributes', $attr, $attachment, $size ); + + $attr = array_map( 'esc_attr', $attr ); + $html = rtrim( " $value ) { + $html .= " $name=" . '"' . $value . '"'; + } + + $html .= ' />'; + } + + /** + * Filters the HTML img element representing an image attachment. + * + * @since 5.6.0 + * + * @param string $html HTML img element or empty string on failure. + * @param int $attachment_id Image attachment ID. + * @param string|int[] $size Requested image size. Can be any registered image size name, or + * an array of width and height values in pixels (in that order). + * @param bool $icon Whether the image should be treated as an icon. + * @param string[] $attr Array of attribute values for the image markup, keyed by attribute name. + * See wp_get_attachment_image(). + */ + return apply_filters( 'wp_get_attachment_image', $html, $attachment_id, $size, $icon, $attr ); +} + +/** + * Gets the URL of an image attachment. + * + * @since 4.4.0 + * + * @param int $attachment_id Image attachment ID. + * @param string|int[] $size Optional. Image size. Accepts any registered image size name, or an array of + * width and height values in pixels (in that order). Default 'thumbnail'. + * @param bool $icon Optional. Whether the image should be treated as an icon. Default false. + * @return string|false Attachment URL or false if no image is available. If `$size` does not match + * any registered image size, the original image URL will be returned. + */ +function wp_get_attachment_image_url( $attachment_id, $size = 'thumbnail', $icon = false ) { + $image = wp_get_attachment_image_src( $attachment_id, $size, $icon ); + return isset( $image[0] ) ? $image[0] : false; +} + +/** + * Gets the attachment path relative to the upload directory. + * + * @since 4.4.1 + * @access private + * + * @param string $file Attachment file name. + * @return string Attachment path relative to the upload directory. + */ +function _wp_get_attachment_relative_path( $file ) { + $dirname = dirname( $file ); + + if ( '.' === $dirname ) { + return ''; + } + + if ( str_contains( $dirname, 'wp-content/uploads' ) ) { + // Get the directory name relative to the upload directory (back compat for pre-2.7 uploads). + $dirname = substr( $dirname, strpos( $dirname, 'wp-content/uploads' ) + 18 ); + $dirname = ltrim( $dirname, '/' ); + } + + return $dirname; +} + +/** + * Gets the image size as array from its meta data. + * + * Used for responsive images. + * + * @since 4.4.0 + * @access private + * + * @param string $size_name Image size. Accepts any registered image size name. + * @param array $image_meta The image meta data. + * @return array|false { + * Array of width and height or false if the size isn't present in the meta data. + * + * @type int $0 Image width. + * @type int $1 Image height. + * } + */ +function _wp_get_image_size_from_meta( $size_name, $image_meta ) { + if ( 'full' === $size_name ) { + return array( + absint( $image_meta['width'] ), + absint( $image_meta['height'] ), + ); + } elseif ( ! empty( $image_meta['sizes'][ $size_name ] ) ) { + return array( + absint( $image_meta['sizes'][ $size_name ]['width'] ), + absint( $image_meta['sizes'][ $size_name ]['height'] ), + ); + } + + return false; +} + +/** + * Retrieves the value for an image attachment's 'srcset' attribute. + * + * @since 4.4.0 + * + * @see wp_calculate_image_srcset() + * + * @param int $attachment_id Image attachment ID. + * @param string|int[] $size Optional. Image size. Accepts any registered image size name, or an array of + * width and height values in pixels (in that order). Default 'medium'. + * @param array $image_meta Optional. The image meta data as returned by 'wp_get_attachment_metadata()'. + * Default null. + * @return string|false A 'srcset' value string or false. + */ +function wp_get_attachment_image_srcset( $attachment_id, $size = 'medium', $image_meta = null ) { + $image = wp_get_attachment_image_src( $attachment_id, $size ); + + if ( ! $image ) { + return false; + } + + if ( ! is_array( $image_meta ) ) { + $image_meta = wp_get_attachment_metadata( $attachment_id ); + } + + $image_src = $image[0]; + $size_array = array( + absint( $image[1] ), + absint( $image[2] ), + ); + + return wp_calculate_image_srcset( $size_array, $image_src, $image_meta, $attachment_id ); +} + +/** + * A helper function to calculate the image sources to include in a 'srcset' attribute. + * + * @since 4.4.0 + * + * @param int[] $size_array { + * An array of width and height values. + * + * @type int $0 The width in pixels. + * @type int $1 The height in pixels. + * } + * @param string $image_src The 'src' of the image. + * @param array $image_meta The image meta data as returned by 'wp_get_attachment_metadata()'. + * @param int $attachment_id Optional. The image attachment ID. Default 0. + * @return string|false The 'srcset' attribute value. False on error or when only one source exists. + */ +function wp_calculate_image_srcset( $size_array, $image_src, $image_meta, $attachment_id = 0 ) { + /** + * Pre-filters the image meta to be able to fix inconsistencies in the stored data. + * + * @since 4.5.0 + * + * @param array $image_meta The image meta data as returned by 'wp_get_attachment_metadata()'. + * @param int[] $size_array { + * An array of requested width and height values. + * + * @type int $0 The width in pixels. + * @type int $1 The height in pixels. + * } + * @param string $image_src The 'src' of the image. + * @param int $attachment_id The image attachment ID or 0 if not supplied. + */ + $image_meta = apply_filters( 'wp_calculate_image_srcset_meta', $image_meta, $size_array, $image_src, $attachment_id ); + + if ( empty( $image_meta['sizes'] ) || ! isset( $image_meta['file'] ) || strlen( $image_meta['file'] ) < 4 ) { + return false; + } + + $image_sizes = $image_meta['sizes']; + + // Get the width and height of 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; + } + + $image_basename = wp_basename( $image_meta['file'] ); + + /* + * WordPress flattens animated GIFs into one frame when generating intermediate sizes. + * To avoid hiding animation in user content, if src is a full size GIF, a srcset attribute is not generated. + * If src is an intermediate size GIF, the full size is excluded from srcset to keep a flattened GIF from becoming animated. + */ + if ( ! isset( $image_sizes['thumbnail']['mime-type'] ) || 'image/gif' !== $image_sizes['thumbnail']['mime-type'] ) { + $image_sizes[] = array( + 'width' => $image_meta['width'], + 'height' => $image_meta['height'], + 'file' => $image_basename, + ); + } elseif ( str_contains( $image_src, $image_meta['file'] ) ) { + return false; + } + + // Retrieve the uploads sub-directory from the full size image. + $dirname = _wp_get_attachment_relative_path( $image_meta['file'] ); + + if ( $dirname ) { + $dirname = trailingslashit( $dirname ); + } + + $upload_dir = wp_get_upload_dir(); + $image_baseurl = trailingslashit( $upload_dir['baseurl'] ) . $dirname; + + /* + * If currently on HTTPS, prefer HTTPS URLs when we know they're supported by the domain + * (which is to say, when they share the domain name of the current request). + */ + if ( is_ssl() && ! str_starts_with( $image_baseurl, 'https' ) && parse_url( $image_baseurl, PHP_URL_HOST ) === $_SERVER['HTTP_HOST'] ) { + $image_baseurl = set_url_scheme( $image_baseurl, 'https' ); + } + + /* + * 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. + */ + $image_edited = preg_match( '/-e[0-9]{13}/', wp_basename( $image_src ), $image_edit_hash ); + + /** + * Filters the maximum image width to be included in a 'srcset' attribute. + * + * @since 4.4.0 + * + * @param int $max_width The maximum image width to be included in the 'srcset'. Default '2048'. + * @param int[] $size_array { + * An array of requested width and height values. + * + * @type int $0 The width in pixels. + * @type int $1 The height in pixels. + * } + */ + $max_srcset_image_width = apply_filters( 'max_srcset_image_width', 2048, $size_array ); + + // Array to hold URL candidates. + $sources = array(); + + /** + * To make sure the ID matches our image src, we will check to see if any sizes in our attachment + * meta match our $image_src. If no matches are found we don't return a srcset to avoid serving + * an incorrect image. See #35045. + */ + $src_matched = false; + + /* + * Loop through available images. Only use images that are resized + * versions of the same edit. + */ + foreach ( $image_sizes as $image ) { + $is_src = false; + + // Check if image meta isn't corrupted. + if ( ! is_array( $image ) ) { + continue; + } + + // If the file name is part of the `src`, we've confirmed a match. + if ( ! $src_matched && str_contains( $image_src, $dirname . $image['file'] ) ) { + $src_matched = true; + $is_src = true; + } + + // Filter out images that are from previous edits. + if ( $image_edited && ! strpos( $image['file'], $image_edit_hash[0] ) ) { + continue; + } + + /* + * Filters out images that are wider than '$max_srcset_image_width' unless + * that file is in the 'src' attribute. + */ + if ( $max_srcset_image_width && $image['width'] > $max_srcset_image_width && ! $is_src ) { + continue; + } + + // If the image dimensions are within 1px of the expected size, use it. + if ( wp_image_matches_ratio( $image_width, $image_height, $image['width'], $image['height'] ) ) { + // Add the URL, descriptor, and value to the sources array to be returned. + $source = array( + 'url' => $image_baseurl . $image['file'], + 'descriptor' => 'w', + 'value' => $image['width'], + ); + + // The 'src' image has to be the first in the 'srcset', because of a bug in iOS8. See #35030. + if ( $is_src ) { + $sources = array( $image['width'] => $source ) + $sources; + } else { + $sources[ $image['width'] ] = $source; + } + } + } + + /** + * Filters an image's 'srcset' sources. + * + * @since 4.4.0 + * + * @param array $sources { + * One or more arrays of source data to include in the 'srcset'. + * + * @type array $width { + * @type string $url The URL of an image source. + * @type string $descriptor The descriptor type used in the image candidate string, + * either 'w' or 'x'. + * @type int $value The source width if paired with a 'w' descriptor, or a + * pixel density value if paired with an 'x' descriptor. + * } + * } + * @param array $size_array { + * An array of requested width and height values. + * + * @type int $0 The width in pixels. + * @type int $1 The height in pixels. + * } + * @param string $image_src The 'src' of the image. + * @param array $image_meta The image meta data as returned by 'wp_get_attachment_metadata()'. + * @param int $attachment_id Image attachment ID or 0. + */ + $sources = apply_filters( 'wp_calculate_image_srcset', $sources, $size_array, $image_src, $image_meta, $attachment_id ); + + // Only return a 'srcset' value if there is more than one source. + if ( ! $src_matched || ! is_array( $sources ) || count( $sources ) < 2 ) { + return false; + } + + $srcset = ''; + + foreach ( $sources as $source ) { + $srcset .= str_replace( ' ', '%20', $source['url'] ) . ' ' . $source['value'] . $source['descriptor'] . ', '; + } + + return rtrim( $srcset, ', ' ); +} + +/** + * Retrieves the value for an image attachment's 'sizes' attribute. + * + * @since 4.4.0 + * + * @see wp_calculate_image_sizes() + * + * @param int $attachment_id Image attachment ID. + * @param string|int[] $size Optional. Image size. Accepts any registered image size name, or an array of + * width and height values in pixels (in that order). Default 'medium'. + * @param array $image_meta Optional. The image meta data as returned by 'wp_get_attachment_metadata()'. + * Default null. + * @return string|false A valid source size value for use in a 'sizes' attribute or false. + */ +function wp_get_attachment_image_sizes( $attachment_id, $size = 'medium', $image_meta = null ) { + $image = wp_get_attachment_image_src( $attachment_id, $size ); + + if ( ! $image ) { + return false; + } + + if ( ! is_array( $image_meta ) ) { + $image_meta = wp_get_attachment_metadata( $attachment_id ); + } + + $image_src = $image[0]; + $size_array = array( + absint( $image[1] ), + absint( $image[2] ), + ); + + return wp_calculate_image_sizes( $size_array, $image_src, $image_meta, $attachment_id ); +} + +/** + * Creates a 'sizes' attribute value for an image. + * + * @since 4.4.0 + * + * @param string|int[] $size Image size. Accepts any registered image size name, or an array of + * width and height values in pixels (in that order). + * @param string $image_src Optional. The URL to the image file. Default null. + * @param array $image_meta Optional. The image meta data as returned by 'wp_get_attachment_metadata()'. + * Default null. + * @param int $attachment_id Optional. Image attachment ID. Either `$image_meta` or `$attachment_id` + * is needed when using the image size name as argument for `$size`. Default 0. + * @return string|false A valid source size value for use in a 'sizes' attribute or false. + */ +function wp_calculate_image_sizes( $size, $image_src = null, $image_meta = null, $attachment_id = 0 ) { + $width = 0; + + if ( is_array( $size ) ) { + $width = absint( $size[0] ); + } elseif ( is_string( $size ) ) { + if ( ! $image_meta && $attachment_id ) { + $image_meta = wp_get_attachment_metadata( $attachment_id ); + } + + if ( is_array( $image_meta ) ) { + $size_array = _wp_get_image_size_from_meta( $size, $image_meta ); + if ( $size_array ) { + $width = absint( $size_array[0] ); + } + } + } + + if ( ! $width ) { + return false; + } + + // Setup the default 'sizes' attribute. + $sizes = sprintf( '(max-width: %1$dpx) 100vw, %1$dpx', $width ); + + /** + * Filters the output of 'wp_calculate_image_sizes()'. + * + * @since 4.4.0 + * + * @param string $sizes A source size value for use in a 'sizes' attribute. + * @param string|int[] $size Requested image size. Can be any registered image size name, or + * an array of width and height values in pixels (in that order). + * @param string|null $image_src The URL to the image file or null. + * @param array|null $image_meta The image meta data as returned by wp_get_attachment_metadata() or null. + * @param int $attachment_id Image attachment ID of the original image or 0. + */ + return apply_filters( 'wp_calculate_image_sizes', $sizes, $size, $image_src, $image_meta, $attachment_id ); +} + +/** + * Determines if the image meta data is for the image source file. + * + * The image meta data is retrieved by attachment post ID. In some cases the post IDs may change. + * For example when the website is exported and imported at another website. Then the + * attachment post IDs that are in post_content for the exported website may not match + * the same attachments at the new website. + * + * @since 5.5.0 + * + * @param string $image_location The full path or URI to the image file. + * @param array $image_meta The attachment meta data as returned by 'wp_get_attachment_metadata()'. + * @param int $attachment_id Optional. The image attachment ID. Default 0. + * @return bool Whether the image meta is for this image file. + */ +function wp_image_file_matches_image_meta( $image_location, $image_meta, $attachment_id = 0 ) { + $match = false; + + // Ensure the $image_meta is valid. + if ( isset( $image_meta['file'] ) && strlen( $image_meta['file'] ) > 4 ) { + // Remove query args in image URI. + list( $image_location ) = explode( '?', $image_location ); + + // Check if the relative image path from the image meta is at the end of $image_location. + if ( strrpos( $image_location, $image_meta['file'] ) === strlen( $image_location ) - strlen( $image_meta['file'] ) ) { + $match = true; + } else { + // Retrieve the uploads sub-directory from the full size image. + $dirname = _wp_get_attachment_relative_path( $image_meta['file'] ); + + if ( $dirname ) { + $dirname = trailingslashit( $dirname ); + } + + if ( ! empty( $image_meta['original_image'] ) ) { + $relative_path = $dirname . $image_meta['original_image']; + + if ( strrpos( $image_location, $relative_path ) === strlen( $image_location ) - strlen( $relative_path ) ) { + $match = true; + } + } + + if ( ! $match && ! empty( $image_meta['sizes'] ) ) { + foreach ( $image_meta['sizes'] as $image_size_data ) { + $relative_path = $dirname . $image_size_data['file']; + + if ( strrpos( $image_location, $relative_path ) === strlen( $image_location ) - strlen( $relative_path ) ) { + $match = true; + break; + } + } + } + } + } + + /** + * Filters whether an image path or URI matches image meta. + * + * @since 5.5.0 + * + * @param bool $match Whether the image relative path from the image meta + * matches the end of the URI or path to the image file. + * @param string $image_location Full path or URI to the tested image file. + * @param array $image_meta The image meta data as returned by 'wp_get_attachment_metadata()'. + * @param int $attachment_id The image attachment ID or 0 if not supplied. + */ + return apply_filters( 'wp_image_file_matches_image_meta', $match, $image_location, $image_meta, $attachment_id ); +} + +/** + * Determines an image's width and height dimensions based on the source file. + * + * @since 5.5.0 + * + * @param string $image_src The image source file. + * @param array $image_meta The image meta data as returned by 'wp_get_attachment_metadata()'. + * @param int $attachment_id Optional. The image attachment ID. Default 0. + * @return array|false Array with first element being the width and second element being the height, + * or false if dimensions cannot be determined. + */ +function wp_image_src_get_dimensions( $image_src, $image_meta, $attachment_id = 0 ) { + $dimensions = false; + + // Is it a full size image? + if ( + isset( $image_meta['file'] ) && + str_contains( $image_src, wp_basename( $image_meta['file'] ) ) + ) { + $dimensions = array( + (int) $image_meta['width'], + (int) $image_meta['height'], + ); + } + + if ( ! $dimensions && ! empty( $image_meta['sizes'] ) ) { + $src_filename = wp_basename( $image_src ); + + foreach ( $image_meta['sizes'] as $image_size_data ) { + if ( $src_filename === $image_size_data['file'] ) { + $dimensions = array( + (int) $image_size_data['width'], + (int) $image_size_data['height'], + ); + + break; + } + } + } + + /** + * Filters the 'wp_image_src_get_dimensions' value. + * + * @since 5.7.0 + * + * @param array|false $dimensions Array with first element being the width + * and second element being the height, or + * false if dimensions could not be determined. + * @param string $image_src The image source file. + * @param array $image_meta The image meta data as returned by + * 'wp_get_attachment_metadata()'. + * @param int $attachment_id The image attachment ID. Default 0. + */ + return apply_filters( 'wp_image_src_get_dimensions', $dimensions, $image_src, $image_meta, $attachment_id ); +} + +/** + * Adds 'srcset' and 'sizes' attributes to an existing 'img' element. + * + * @since 4.4.0 + * + * @see wp_calculate_image_srcset() + * @see wp_calculate_image_sizes() + * + * @param string $image An HTML 'img' element to be filtered. + * @param array $image_meta The image meta data as returned by 'wp_get_attachment_metadata()'. + * @param int $attachment_id Image attachment ID. + * @return string Converted 'img' element with 'srcset' and 'sizes' attributes added. + */ +function wp_image_add_srcset_and_sizes( $image, $image_meta, $attachment_id ) { + // Ensure the image meta exists. + if ( empty( $image_meta['sizes'] ) ) { + return $image; + } + + $image_src = preg_match( '/src="([^"]+)"/', $image, $match_src ) ? $match_src[1] : ''; + list( $image_src ) = explode( '?', $image_src ); + + // Return early if we couldn't get the image source. + if ( ! $image_src ) { + return $image; + } + + // Bail early if an image has been inserted and later edited. + if ( preg_match( '/-e[0-9]{13}/', $image_meta['file'], $img_edit_hash ) + && ! str_contains( wp_basename( $image_src ), $img_edit_hash[0] ) + ) { + 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 ) { + $size_array = array( $width, $height ); + } else { + $size_array = wp_image_src_get_dimensions( $image_src, $image_meta, $attachment_id ); + if ( ! $size_array ) { + return $image; + } + } + + $srcset = wp_calculate_image_srcset( $size_array, $image_src, $image_meta, $attachment_id ); + + if ( $srcset ) { + // Check if there is already a 'sizes' attribute. + $sizes = strpos( $image, ' sizes=' ); + + if ( ! $sizes ) { + $sizes = wp_calculate_image_sizes( $size_array, $image_src, $image_meta, $attachment_id ); + } + } + + if ( $srcset && $sizes ) { + // Format the 'srcset' and 'sizes' string and escape attributes. + $attr = sprintf( ' srcset="%s"', esc_attr( $srcset ) ); + + if ( is_string( $sizes ) ) { + $attr .= sprintf( ' sizes="%s"', esc_attr( $sizes ) ); + } + + // Add the srcset and sizes attributes to the image markup. + return preg_replace( '/]+?)[\/ ]*>/', '', $image ); + } + + return $image; +} + +/** + * Determines whether to add the `loading` attribute to the specified tag in the specified context. + * + * @since 5.5.0 + * @since 5.7.0 Now returns `true` by default for `iframe` tags. + * + * @param string $tag_name The tag name. + * @param string $context Additional context, like the current filter name + * or the function name from where this was called. + * @return bool Whether to add the attribute. + */ +function wp_lazy_loading_enabled( $tag_name, $context ) { + /* + * By default add to all 'img' and 'iframe' tags. + * See https://html.spec.whatwg.org/multipage/embedded-content.html#attr-img-loading + * See https://html.spec.whatwg.org/multipage/iframe-embed-object.html#attr-iframe-loading + */ + $default = ( 'img' === $tag_name || 'iframe' === $tag_name ); + + /** + * Filters whether to add the `loading` attribute to the specified tag in the specified context. + * + * @since 5.5.0 + * + * @param bool $default Default value. + * @param string $tag_name The tag name. + * @param string $context Additional context, like the current filter name + * or the function name from where this was called. + */ + return (bool) apply_filters( 'wp_lazy_loading_enabled', $default, $tag_name, $context ); +} + +/** + * Filters specific tags in post content and modifies their markup. + * + * Modifies HTML tags in post content to include new browser and HTML technologies + * that may not have existed at the time of post creation. These modifications currently + * include adding `srcset`, `sizes`, and `loading` attributes to `img` HTML tags, as well + * as adding `loading` attributes to `iframe` HTML tags. + * Future similar optimizations should be added/expected here. + * + * @since 5.5.0 + * @since 5.7.0 Now supports adding `loading` attributes to `iframe` tags. + * + * @see wp_img_tag_add_width_and_height_attr() + * @see wp_img_tag_add_srcset_and_sizes_attr() + * @see wp_img_tag_add_loading_optimization_attrs() + * @see wp_iframe_tag_add_loading_attr() + * + * @param string $content The HTML content to be filtered. + * @param string $context Optional. Additional context to pass to the filters. + * Defaults to `current_filter()` when not set. + * @return string Converted content with images modified. + */ +function wp_filter_content_tags( $content, $context = null ) { + if ( null === $context ) { + $context = current_filter(); + } + + $add_iframe_loading_attr = wp_lazy_loading_enabled( 'iframe', $context ); + + if ( ! preg_match_all( '/<(img|iframe)\s[^>]+>/', $content, $matches, PREG_SET_ORDER ) ) { + return $content; + } + + // List of the unique `img` tags found in $content. + $images = array(); + + // List of the unique `iframe` tags found in $content. + $iframes = array(); + + foreach ( $matches as $match ) { + list( $tag, $tag_name ) = $match; + + switch ( $tag_name ) { + case 'img': + if ( preg_match( '/wp-image-([0-9]+)/i', $tag, $class_id ) ) { + $attachment_id = absint( $class_id[1] ); + + if ( $attachment_id ) { + /* + * If exactly the same image tag is used more than once, overwrite it. + * All identical tags will be replaced later with 'str_replace()'. + */ + $images[ $tag ] = $attachment_id; + break; + } + } + $images[ $tag ] = 0; + break; + case 'iframe': + $iframes[ $tag ] = 0; + break; + } + } + + // Reduce the array to unique attachment IDs. + $attachment_ids = array_unique( array_filter( array_values( $images ) ) ); + + if ( count( $attachment_ids ) > 1 ) { + /* + * Warm the object cache with post and meta information for all found + * images to avoid making individual database calls. + */ + _prime_post_caches( $attachment_ids, false, true ); + } + + // Iterate through the matches in order of occurrence as it is relevant for whether or not to lazy-load. + foreach ( $matches as $match ) { + // Filter an image match. + if ( isset( $images[ $match[0] ] ) ) { + $filtered_image = $match[0]; + $attachment_id = $images[ $match[0] ]; + + // Add 'width' and 'height' attributes if applicable. + if ( $attachment_id > 0 && ! str_contains( $filtered_image, ' width=' ) && ! str_contains( $filtered_image, ' height=' ) ) { + $filtered_image = wp_img_tag_add_width_and_height_attr( $filtered_image, $context, $attachment_id ); + } + + // Add 'srcset' and 'sizes' attributes if applicable. + if ( $attachment_id > 0 && ! str_contains( $filtered_image, ' srcset=' ) ) { + $filtered_image = wp_img_tag_add_srcset_and_sizes_attr( $filtered_image, $context, $attachment_id ); + } + + // Add loading optimization attributes if applicable. + $filtered_image = wp_img_tag_add_loading_optimization_attrs( $filtered_image, $context ); + + /** + * Filters an img tag within the content for a given context. + * + * @since 6.0.0 + * + * @param string $filtered_image Full img tag with attributes that will replace the source img tag. + * @param string $context Additional context, like the current filter name or the function name from where this was called. + * @param int $attachment_id The image attachment ID. May be 0 in case the image is not an attachment. + */ + $filtered_image = apply_filters( 'wp_content_img_tag', $filtered_image, $context, $attachment_id ); + + if ( $filtered_image !== $match[0] ) { + $content = str_replace( $match[0], $filtered_image, $content ); + } + + /* + * Unset image lookup to not run the same logic again unnecessarily if the same image tag is used more than + * once in the same blob of content. + */ + unset( $images[ $match[0] ] ); + } + + // Filter an iframe match. + if ( isset( $iframes[ $match[0] ] ) ) { + $filtered_iframe = $match[0]; + + // Add 'loading' attribute if applicable. + if ( $add_iframe_loading_attr && ! str_contains( $filtered_iframe, ' loading=' ) ) { + $filtered_iframe = wp_iframe_tag_add_loading_attr( $filtered_iframe, $context ); + } + + if ( $filtered_iframe !== $match[0] ) { + $content = str_replace( $match[0], $filtered_iframe, $content ); + } + + /* + * Unset iframe lookup to not run the same logic again unnecessarily if the same iframe tag is used more + * than once in the same blob of content. + */ + unset( $iframes[ $match[0] ] ); + } + } + + return $content; +} + +/** + * Adds optimization attributes to an `img` HTML tag. + * + * @since 6.3.0 + * + * @param string $image The HTML `img` tag where the attribute should be added. + * @param string $context Additional context to pass to the filters. + * @return string Converted `img` tag with optimization attributes added. + */ +function wp_img_tag_add_loading_optimization_attrs( $image, $context ) { + $width = preg_match( '/ width=["\']([0-9]+)["\']/', $image, $match_width ) ? (int) $match_width[1] : null; + $height = preg_match( '/ height=["\']([0-9]+)["\']/', $image, $match_height ) ? (int) $match_height[1] : null; + $loading_val = preg_match( '/ loading=["\']([A-Za-z]+)["\']/', $image, $match_loading ) ? $match_loading[1] : null; + $fetchpriority_val = preg_match( '/ fetchpriority=["\']([A-Za-z]+)["\']/', $image, $match_fetchpriority ) ? $match_fetchpriority[1] : null; + $decoding_val = preg_match( '/ decoding=["\']([A-Za-z]+)["\']/', $image, $match_decoding ) ? $match_decoding[1] : null; + + /* + * Get loading optimization attributes to use. + * This must occur before the conditional check below so that even images + * that are ineligible for being lazy-loaded are considered. + */ + $optimization_attrs = wp_get_loading_optimization_attributes( + 'img', + array( + 'width' => $width, + 'height' => $height, + 'loading' => $loading_val, + 'fetchpriority' => $fetchpriority_val, + 'decoding' => $decoding_val, + ), + $context + ); + + // Images should have source for the loading optimization attributes to be added. + if ( ! str_contains( $image, ' src="' ) ) { + return $image; + } + + if ( empty( $decoding_val ) ) { + /** + * Filters the `decoding` attribute value to add to an image. Default `async`. + * + * Returning a falsey value will omit the attribute. + * + * @since 6.1.0 + * + * @param string|false|null $value The `decoding` attribute value. Returning a falsey value + * will result in the attribute being omitted for the image. + * Otherwise, it may be: 'async', 'sync', or 'auto'. Defaults to false. + * @param string $image The HTML `img` tag to be filtered. + * @param string $context Additional context about how the function was called + * or where the img tag is. + */ + $filtered_decoding_attr = apply_filters( + 'wp_img_tag_add_decoding_attr', + isset( $optimization_attrs['decoding'] ) ? $optimization_attrs['decoding'] : false, + $image, + $context + ); + + // Validate the values after filtering. + if ( isset( $optimization_attrs['decoding'] ) && ! $filtered_decoding_attr ) { + // Unset `decoding` attribute if `$filtered_decoding_attr` is set to `false`. + unset( $optimization_attrs['decoding'] ); + } elseif ( in_array( $filtered_decoding_attr, array( 'async', 'sync', 'auto' ), true ) ) { + $optimization_attrs['decoding'] = $filtered_decoding_attr; + } + + if ( ! empty( $optimization_attrs['decoding'] ) ) { + $image = str_replace( '