$created_sizes = $editor->multi_resize( $new_sizes );
if ( ! empty( $created_sizes ) ) {
$image_meta['sizes'] = array_merge( $image_meta['sizes'], $created_sizes );
wp_update_attachment_metadata( $attachment_id, $image_meta );
* Copy parent attachment properties to newly cropped image.
* @param string $cropped Path to the cropped image file.
* @param int $parent_attachment_id Parent file Attachment ID.
* @param string $context Control calling the function.
* @return array Properties of attachment.
function wp_copy_parent_attachment_properties( $cropped, $parent_attachment_id, $context = '' ) {
$parent = get_post( $parent_attachment_id );
$parent_url = wp_get_attachment_url( $parent->ID );
$parent_basename = wp_basename( $parent_url );
$url = str_replace( wp_basename( $parent_url ), wp_basename( $cropped ), $parent_url );
$size = wp_getimagesize( $cropped );
$image_type = $size ? $size['mime'] : 'image/jpeg';
$sanitized_post_title = sanitize_file_name( $parent->post_title );
( '' !== trim( $parent->post_title ) ) &&
* Check if the original image has a title other than the "filename" default,
* meaning the image had a title when originally uploaded or its title was edited.
( $parent_basename !== $sanitized_post_title ) &&
( pathinfo( $parent_basename, PATHINFO_FILENAME ) !== $sanitized_post_title )
$use_original_description = ( '' !== trim( $parent->post_content ) );
'post_title' => $use_original_title ? $parent->post_title : wp_basename( $cropped ),
'post_content' => $use_original_description ? $parent->post_content : $url,
'post_mime_type' => $image_type,
// Copy the image caption attribute (post_excerpt field) from the original image.
if ( '' !== trim( $parent->post_excerpt ) ) {
$attachment['post_excerpt'] = $parent->post_excerpt;
// Copy the image alt text attribute from the original image.
if ( '' !== trim( $parent->_wp_attachment_image_alt ) ) {
$attachment['meta_input'] = array(
'_wp_attachment_image_alt' => wp_slash( $parent->_wp_attachment_image_alt ),
$attachment['post_parent'] = $parent_attachment_id;
* Generates attachment meta data and create image sub-sizes for images.
* @since 6.0.0 The `$filesize` value was added to the returned array.
* @since 6.7.0 The 'image/heic' mime type is supported.
* @param int $attachment_id Attachment ID to process.
* @param string $file Filepath of the attached image.
* @return array Metadata for attachment.
function wp_generate_attachment_metadata( $attachment_id, $file ) {
$attachment = get_post( $attachment_id );
$mime_type = get_post_mime_type( $attachment );
if ( 'image/heic' === $mime_type || ( preg_match( '!^image/!', $mime_type ) && file_is_displayable_image( $file ) ) ) {
// Make thumbnails and other intermediate sizes.
$metadata = wp_create_image_subsizes( $file, $attachment_id );
} elseif ( wp_attachment_is( 'video', $attachment ) ) {
$metadata = wp_read_video_metadata( $file );
$support = current_theme_supports( 'post-thumbnails', 'attachment:video' ) || post_type_supports( 'attachment:video', 'thumbnail' );
} elseif ( wp_attachment_is( 'audio', $attachment ) ) {
$metadata = wp_read_audio_metadata( $file );
$support = current_theme_supports( 'post-thumbnails', 'attachment:audio' ) || post_type_supports( 'attachment:audio', 'thumbnail' );
* wp_read_video_metadata() and wp_read_audio_metadata() return `false`
* if the attachment does not exist in the local filesystem,
* so make sure to convert the value to an array.
if ( ! is_array( $metadata ) ) {
if ( $support && ! empty( $metadata['image']['data'] ) ) {
// Check for existing cover.
$hash = md5( $metadata['image']['data'] );
'post_type' => 'attachment',
'post_mime_type' => $metadata['image']['mime'],
'post_status' => 'inherit',
'meta_key' => '_cover_hash',
$exists = reset( $posts );
if ( ! empty( $exists ) ) {
update_post_meta( $attachment_id, '_thumbnail_id', $exists );
switch ( $metadata['image']['mime'] ) {
$basename = str_replace( '.', '-', wp_basename( $file ) ) . '-image' . $ext;
$uploaded = wp_upload_bits( $basename, '', $metadata['image']['data'] );
if ( false === $uploaded['error'] ) {
$image_attachment = array(
'post_mime_type' => $metadata['image']['mime'],
'post_type' => 'attachment',
* Filters the parameters for the attachment thumbnail creation.
* @param array $image_attachment An array of parameters to create the thumbnail.
* @param array $metadata Current attachment metadata.
* @param array $uploaded {
* Information about the newly-uploaded file.
* @type string $file Filename of the newly-uploaded file.
* @type string $url URL of the uploaded file.
* @type string $type File type.
$image_attachment = apply_filters( 'attachment_thumbnail_args', $image_attachment, $metadata, $uploaded );
$sub_attachment_id = wp_insert_attachment( $image_attachment, $uploaded['file'] );
add_post_meta( $sub_attachment_id, '_cover_hash', $hash );
$attach_data = wp_generate_attachment_metadata( $sub_attachment_id, $uploaded['file'] );
wp_update_attachment_metadata( $sub_attachment_id, $attach_data );
update_post_meta( $attachment_id, '_thumbnail_id', $sub_attachment_id );
} elseif ( 'application/pdf' === $mime_type ) {
// Try to create image thumbnails for PDFs.
* Filters the image sizes generated for non-image mime types.
* @param string[] $fallback_sizes An array of image size names.
* @param array $metadata Current attachment metadata.
$fallback_sizes = apply_filters( 'fallback_intermediate_image_sizes', $fallback_sizes, $metadata );
$registered_sizes = wp_get_registered_image_subsizes();
$merged_sizes = array_intersect_key( $registered_sizes, array_flip( $fallback_sizes ) );
// Force thumbnails to be soft crops.
if ( isset( $merged_sizes['thumbnail'] ) && is_array( $merged_sizes['thumbnail'] ) ) {
$merged_sizes['thumbnail']['crop'] = false;
// Only load PDFs in an image editor if we're processing sizes.
if ( ! empty( $merged_sizes ) ) {
$editor = wp_get_image_editor( $file );
if ( ! is_wp_error( $editor ) ) { // No support for this type of file.
* PDFs may have the same file filename as JPEGs.
* Ensure the PDF preview image does not overwrite any JPEG images that already exist.
$dirname = dirname( $file ) . '/';
$ext = '.' . pathinfo( $file, PATHINFO_EXTENSION );
$preview_file = $dirname . wp_unique_filename( $dirname, wp_basename( $file, $ext ) . '-pdf.jpg' );
$uploaded = $editor->save( $preview_file, 'image/jpeg' );
// Resize based on the full size image, rather than the source.
if ( ! is_wp_error( $uploaded ) ) {
$image_file = $uploaded['path'];
unset( $uploaded['path'] );
$metadata['sizes'] = array(
// Save the meta data before any image post-processing errors could happen.
wp_update_attachment_metadata( $attachment_id, $metadata );
// Create sub-sizes saving the image meta after each.
$metadata = _wp_make_subsizes( $merged_sizes, $image_file, $metadata, $attachment_id );
// Remove the blob of binary data from the array.
unset( $metadata['image']['data'] );
// Capture file size for cases where it has not been captured yet, such as PDFs.
if ( ! isset( $metadata['filesize'] ) && file_exists( $file ) ) {
$metadata['filesize'] = wp_filesize( $file );
* Filters the generated attachment meta data.
* @since 5.3.0 The `$context` parameter was added.
* @param array $metadata An array of attachment meta data.
* @param int $attachment_id Current attachment ID.
* @param string $context Additional context. Can be 'create' when metadata was initially created for new attachment
* or 'update' when the metadata was updated.
return apply_filters( 'wp_generate_attachment_metadata', $metadata, $attachment_id, 'create' );
* Converts a fraction string to a decimal.
* @param string $str Fraction string.
* @return int|float Returns calculated fraction or integer 0 on invalid input.
function wp_exif_frac2dec( $str ) {
if ( ! is_scalar( $str ) || is_bool( $str ) ) {
if ( ! is_string( $str ) ) {
return $str; // This can only be an integer or float, so this is fine.
// Fractions passed as a string must contain a single `/`.
if ( substr_count( $str, '/' ) !== 1 ) {
if ( is_numeric( $str ) ) {
list( $numerator, $denominator ) = explode( '/', $str );
// Both the numerator and the denominator must be numbers.
if ( ! is_numeric( $numerator ) || ! is_numeric( $denominator ) ) {
// The denominator must not be zero.
if ( 0 == $denominator ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual -- Deliberate loose comparison.
return $numerator / $denominator;
* Converts the exif date format to a unix timestamp.
* @param string $str A date string expected to be in Exif format (Y:m:d H:i:s).
* @return int|false The unix timestamp, or false on failure.
function wp_exif_date2ts( $str ) {
list( $date, $time ) = explode( ' ', trim( $str ) );
list( $y, $m, $d ) = explode( ':', $date );
return strtotime( "{$y}-{$m}-{$d} {$time}" );
* Gets extended image metadata, exif or iptc as available.
* Retrieves the EXIF metadata aperture, credit, camera, caption, copyright, iso
* created_timestamp, focal_length, shutter_speed, and title.
* The IPTC metadata that is retrieved is APP13, credit, byline, created date
* and time, caption, copyright, and title. Also includes FNumber, Model,
* DateTimeDigitized, FocalLength, ISOSpeedRatings, and ExposureTime.
* @todo Try other exif libraries if available.
* @return array|false Image metadata array on success, false on failure.
function wp_read_image_metadata( $file ) {
if ( ! file_exists( $file ) ) {
$image_size = wp_getimagesize( $file );
if ( false === $image_size ) {
list( , , $image_type ) = $image_size;
* EXIF contains a bunch of data we'll probably never need formatted in ways
* that are difficult to use. We'll normalize it and just extract the fields
* that are likely to be useful. Fractions and numbers are converted to
* floats, dates to unix timestamps, and everything else to strings.
'created_timestamp' => 0,
* Read IPTC first, since it might contain data not available in exif such
* as caption, description etc.
if ( is_callable( 'iptcparse' ) ) {
wp_getimagesize( $file, $info );
if ( ! empty( $info['APP13'] ) ) {
// Don't silence errors when in debug mode, unless running unit tests.
if ( defined( 'WP_DEBUG' ) && WP_DEBUG
&& ! defined( 'WP_RUN_CORE_TESTS' )
$iptc = iptcparse( $info['APP13'] );
// Silencing notice and warning is intentional. See https://core.trac.wordpress.org/ticket/42480
$iptc = @iptcparse( $info['APP13'] );
if ( ! is_array( $iptc ) ) {
// Headline, "A brief synopsis of the caption".
if ( ! empty( $iptc['2#105'][0] ) ) {
$meta['title'] = trim( $iptc['2#105'][0] );
* Title, "Many use the Title field to store the filename of the image,
* though the field may be used in many ways".
} elseif ( ! empty( $iptc['2#005'][0] ) ) {
$meta['title'] = trim( $iptc['2#005'][0] );
if ( ! empty( $iptc['2#120'][0] ) ) { // Description / legacy caption.
$caption = trim( $iptc['2#120'][0] );
mbstring_binary_safe_encoding();
$caption_length = strlen( $caption );
reset_mbstring_encoding();
if ( empty( $meta['title'] ) && $caption_length < 80 ) {
// Assume the title is stored in 2:120 if it's short.
$meta['title'] = $caption;
$meta['caption'] = $caption;
if ( ! empty( $iptc['2#110'][0] ) ) { // Credit.
$meta['credit'] = trim( $iptc['2#110'][0] );
} elseif ( ! empty( $iptc['2#080'][0] ) ) { // Creator / legacy byline.
$meta['credit'] = trim( $iptc['2#080'][0] );
if ( ! empty( $iptc['2#055'][0] ) && ! empty( $iptc['2#060'][0] ) ) { // Created date and time.
$meta['created_timestamp'] = strtotime( $iptc['2#055'][0] . ' ' . $iptc['2#060'][0] );
if ( ! empty( $iptc['2#116'][0] ) ) { // Copyright.
$meta['copyright'] = trim( $iptc['2#116'][0] );
if ( ! empty( $iptc['2#025'][0] ) ) { // Keywords array.
$meta['keywords'] = array_values( $iptc['2#025'] );
* Filters the image types to check for exif data.
* @param int[] $image_types Array of image types to check for exif data. Each value
* is usually one of the `IMAGETYPE_*` constants.
$exif_image_types = apply_filters( 'wp_read_image_metadata_types', array( IMAGETYPE_JPEG, IMAGETYPE_TIFF_II, IMAGETYPE_TIFF_MM ) );
if ( is_callable( 'exif_read_data' ) && in_array( $image_type, $exif_image_types, true ) ) {
// Don't silence errors when in debug mode, unless running unit tests.
if ( defined( 'WP_DEBUG' ) && WP_DEBUG
&& ! defined( 'WP_RUN_CORE_TESTS' )
$exif = exif_read_data( $file );
// Silencing notice and warning is intentional. See https://core.trac.wordpress.org/ticket/42480
$exif = @exif_read_data( $file );
if ( ! is_array( $exif ) ) {
if ( ! empty( $exif['ImageDescription'] ) ) {
$exif_description = trim( $exif['ImageDescription'] );
if ( ! empty( $exif['COMPUTED']['UserComment'] ) ) {
$exif_usercomment = trim( $exif['COMPUTED']['UserComment'] );
if ( $exif_description ) {
mbstring_binary_safe_encoding();
$description_length = strlen( $exif_description );
reset_mbstring_encoding();
if ( empty( $meta['title'] ) && $description_length < 80 ) {
// Assume the title is stored in ImageDescription.
$meta['title'] = $exif_description;
// If both user comments and description are present.
if ( empty( $meta['caption'] ) && $exif_usercomment ) {
if ( ! empty( $meta['title'] ) && $exif_description === $meta['title'] ) {
$caption = $exif_usercomment;
if ( $exif_description === $exif_usercomment ) {
$caption = $exif_description;
$caption = trim( $exif_description . ' ' . $exif_usercomment );
$meta['caption'] = $caption;
if ( empty( $meta['caption'] ) && $exif_usercomment ) {
$meta['caption'] = $exif_usercomment;
if ( empty( $meta['caption'] ) ) {
$meta['caption'] = $exif_description;
} elseif ( empty( $meta['caption'] ) && $exif_usercomment ) {
$meta['caption'] = $exif_usercomment;
$description_length = strlen( $exif_usercomment );
if ( empty( $meta['title'] ) && $description_length < 80 ) {
$meta['title'] = trim( $exif_usercomment );