<?php

use WP_CLI\Utils;

/**
 * Imports files as attachments, regenerates thumbnails, or lists registered image sizes.
 *
 * ## EXAMPLES
 *
 *     # Re-generate all thumbnails, without confirmation.
 *     $ wp media regenerate --yes
 *     Found 3 images to regenerate.
 *     1/3 Regenerated thumbnails for "Sydney Harbor Bridge" (ID 760).
 *     2/3 Regenerated thumbnails for "Boardwalk" (ID 757).
 *     3/3 Regenerated thumbnails for "Sunburst Over River" (ID 756).
 *     Success: Regenerated 3 of 3 images.
 *
 *     # Import a local image and set it to be the featured image for a post.
 *     $ wp media import ~/Downloads/image.png --post_id=123 --title="A downloaded picture" --featured_image
 *     Imported file '/home/person/Downloads/image.png' as attachment ID 1753 and attached to post 123 as featured image.
 *     Success: Imported 1 of 1 images.
 *
 *     # List all registered image sizes
 *     $ wp media image-size
 *     +---------------------------+-------+--------+-------+
 *     | name                      | width | height | crop  |
 *     +---------------------------+-------+--------+-------+
 *     | full                      |       |        | N/A   |
 *     | twentyfourteen-full-width | 1038  | 576    | hard  |
 *     | large                     | 1024  | 1024   | soft  |
 *     | medium_large              | 768   | 0      | soft  |
 *     | medium                    | 300   | 300    | soft  |
 *     | thumbnail                 | 150   | 150    | hard  |
 *     +---------------------------+-------+--------+-------+
 *
 *     # Fix orientation for specific images.
 *     $ wp media fix-orientation 63
 *     1/1 Fixing orientation for "Portrait_6" (ID 63).
 *     Success: Fixed 1 of 1 images.
 *
 * @package wp-cli
 */
class Media_Command extends WP_CLI_Command {

	/**
	 * Clear the WP object cache after this many regenerations/imports.
	 *
	 * @var integer
	 */
	const WP_CLEAR_OBJECT_CACHE_INTERVAL = 500;

	/**
	 * Regenerates thumbnails for one or more attachments.
	 *
	 * ## OPTIONS
	 *
	 * [<attachment-id>...]
	 * : One or more IDs of the attachments to regenerate.
	 *
	 * [--image_size=<image_size>]
	 * : Name of the image size to regenerate. Only thumbnails of this image size will be regenerated, thumbnails of other image sizes will not.
	 *
	 * [--skip-delete]
	 * : Skip deletion of the original thumbnails. If your thumbnails are linked from sources outside your control, it's likely best to leave them around. Defaults to false.
	 *
	 * [--only-missing]
	 * : Only generate thumbnails for images missing image sizes.
	 *
	 * [--delete-unknown]
	 * : Only delete thumbnails for old unregistered image sizes.
	 *
	 * [--yes]
	 * : Answer yes to the confirmation message. Confirmation only shows when no IDs passed as arguments.
	 *
	 * ## EXAMPLES
	 *
	 *     # Regenerate thumbnails for given attachment IDs.
	 *     $ wp media regenerate 123 124 125
	 *     Found 3 images to regenerate.
	 *     1/3 Regenerated thumbnails for "Vertical Image" (ID 123).
	 *     2/3 Regenerated thumbnails for "Horizontal Image" (ID 124).
	 *     3/3 Regenerated thumbnails for "Beautiful Picture" (ID 125).
	 *     Success: Regenerated 3 of 3 images.
	 *
	 *     # Regenerate all thumbnails, without confirmation.
	 *     $ wp media regenerate --yes
	 *     Found 3 images to regenerate.
	 *     1/3 Regenerated thumbnails for "Sydney Harbor Bridge" (ID 760).
	 *     2/3 Regenerated thumbnails for "Boardwalk" (ID 757).
	 *     3/3 Regenerated thumbnails for "Sunburst Over River" (ID 756).
	 *     Success: Regenerated 3 of 3 images.
	 *
	 *     # Re-generate all thumbnails that have IDs between 1000 and 2000.
	 *     $ seq 1000 2000 | xargs wp media regenerate
	 *     Found 4 images to regenerate.
	 *     1/4 Regenerated thumbnails for "Vertical Featured Image" (ID 1027).
	 *     2/4 Regenerated thumbnails for "Horizontal Featured Image" (ID 1022).
	 *     3/4 Regenerated thumbnails for "Unicorn Wallpaper" (ID 1045).
	 *     4/4 Regenerated thumbnails for "I Am Worth Loving Wallpaper" (ID 1023).
	 *     Success: Regenerated 4 of 4 images.
	 *
	 *     # Re-generate only the thumbnails of "large" image size for all images.
	 *     $ wp media regenerate --image_size=large
	 *     Do you really want to regenerate the "large" image size for all images? [y/n] y
	 *     Found 3 images to regenerate.
	 *     1/3 Regenerated "large" thumbnail for "Sydney Harbor Bridge" (ID 760).
	 *     2/3 No "large" thumbnail regeneration needed for "Boardwalk" (ID 757).
	 *     3/3 Regenerated "large" thumbnail for "Sunburst Over River" (ID 756).
	 *     Success: Regenerated 3 of 3 images.
	 */
	public function regenerate( $args, $assoc_args = array() ) {
		$assoc_args = wp_parse_args(
			$assoc_args,
			[ 'image_size' => '' ]
		);

		$image_size = $assoc_args['image_size'];
		if ( $image_size && ! in_array( $image_size, get_intermediate_image_sizes(), true ) ) {
			WP_CLI::error( sprintf( 'Unknown image size "%s".', $image_size ) );
		}

		if ( empty( $args ) ) {
			if ( $image_size ) {
				WP_CLI::confirm( sprintf( 'Do you really want to regenerate the "%s" image size for all images?', $image_size ), $assoc_args );
			} else {
				WP_CLI::confirm( 'Do you really want to regenerate all images?', $assoc_args );
			}
		}

		$skip_delete  = Utils\get_flag_value( $assoc_args, 'skip-delete' );
		$only_missing = Utils\get_flag_value( $assoc_args, 'only-missing' );
		if ( $only_missing ) {
			$skip_delete = true;
		}

		$delete_unknown = Utils\get_flag_value( $assoc_args, 'delete-unknown' );
		if ( $delete_unknown ) {
			$skip_delete = false;
		}

		$additional_mime_types = array();

		if ( Utils\wp_version_compare( '4.7', '>=' ) ) {
			$additional_mime_types[] = 'application/pdf';
		}

		$images = $this->get_images( $args, $additional_mime_types );
		$count  = $images->post_count;

		if ( ! $count ) {
			WP_CLI::warning( 'No images found.' );
			return;
		}

		WP_CLI::log(
			sprintf(
				'Found %1$d %2$s to regenerate.',
				$count,
				_n( 'image', 'images', $count )
			)
		);

		if ( $image_size ) {
			$image_size_filters = $this->add_image_size_filters( $image_size );
		}

		$number    = 0;
		$successes = 0;
		$errors    = 0;
		$skips     = 0;
		foreach ( $images->posts as $post_id ) {
			++$number;
			if ( 0 === $number % self::WP_CLEAR_OBJECT_CACHE_INTERVAL ) {
				Utils\wp_clear_object_cache();
			}
			$this->process_regeneration( $post_id, $skip_delete, $only_missing, $delete_unknown, $image_size, $number . '/' . $count, $successes, $errors, $skips );
		}

		if ( $image_size ) {
			$this->remove_image_size_filters( $image_size_filters );
		}

		Utils\report_batch_operation_results( 'image', 'regenerate', $count, $successes, $errors, $skips );
	}

	/**
	 * Creates attachments from local files or URLs.
	 *
	 * ## OPTIONS
	 *
	 * <file>...
	 * : Path to file or files to be imported. Supports the glob(3) capabilities of the current shell.
	 *     If file is recognized as a URL (for example, with a scheme of http or ftp), the file will be
	 *     downloaded to a temp file before being sideloaded.
	 *
	 * [--post_id=<post_id>]
	 * : ID of the post to attach the imported files to.
	 *
	 * [--post_name=<post_name>]
	 * : Name of the post to attach the imported files to.
	 *
	 * [--file_name=<name>]
	 * : Attachment name (post_name field).
	 *
	 * [--title=<title>]
	 * : Attachment title (post title field).
	 *
	 * [--caption=<caption>]
	 * : Caption for attachment (post excerpt field).
	 *
	 * [--alt=<alt_text>]
	 * : Alt text for image (saved as post meta).
	 *
	 * [--desc=<description>]
	 * : "Description" field (post content) of attachment post.
	 *
	 * [--skip-copy]
	 * : If set, media files (local only) are imported to the library but not moved on disk.
	 * File names will not be run through wp_unique_filename() with this set.
	 *
	 * [--preserve-filetime]
	 * : Use the file modified time as the post published & modified dates.
	 * Remote files will always use the current time.
	 *
	 * [--featured_image]
	 * : If set, set the imported image as the Featured Image of the post it is attached to.
	 *
	 * [--porcelain[=<field>]]
	 * : Output a single field for each imported image. Defaults to attachment ID when used as flag.
	 * ---
	 * options:
	 *   - url
	 * ---
	 *
	 * ## EXAMPLES
	 *
	 *     # Import all jpgs in the current user's "Pictures" directory, not attached to any post.
	 *     $ wp media import ~/Pictures/**\/*.jpg
	 *     Imported file '/home/person/Pictures/landscape-photo.jpg' as attachment ID 1751.
	 *     Imported file '/home/person/Pictures/fashion-icon.jpg' as attachment ID 1752.
	 *     Success: Imported 2 of 2 items.
	 *
	 *     # Import a local image and set it to be the post thumbnail for a post.
	 *     $ wp media import ~/Downloads/image.png --post_id=123 --title="A downloaded picture" --featured_image
	 *     Imported file '/home/person/Downloads/image.png' as attachment ID 1753 and attached to post 123 as featured image.
	 *     Success: Imported 1 of 1 images.
	 *
	 *     # Import a local image, but set it as the featured image for all posts.
	 *     # 1. Import the image and get its attachment ID.
	 *     # 2. Assign the attachment ID as the featured image for all posts.
	 *     $ ATTACHMENT_ID="$(wp media import ~/Downloads/image.png --porcelain)"
	 *     $ wp post list --post_type=post --format=ids | xargs -d ' ' -I % wp post meta add % _thumbnail_id $ATTACHMENT_ID
	 *     Success: Added custom field.
	 *     Success: Added custom field.
	 *
	 *     # Import an image from the web.
	 *     $ wp media import http://s.wordpress.org/style/images/wp-header-logo.png --title='The WordPress logo' --alt="Semantic personal publishing"
	 *     Imported file 'http://s.wordpress.org/style/images/wp-header-logo.png' as attachment ID 1755.
	 *     Success: Imported 1 of 1 images.
	 *
	 *     # Get the URL for an attachment after import.
	 *     $ wp media import http://s.wordpress.org/style/images/wp-header-logo.png --porcelain | xargs -I {} wp post list --post__in={} --field=url --post_type=attachment
	 *     http://wordpress-develop.dev/wp-header-logo/
	 */
	public function import( $args, $assoc_args = array() ) {
		$assoc_args = wp_parse_args(
			$assoc_args,
			array(
				'file_name' => '',
				'title'     => '',
				'caption'   => '',
				'alt'       => '',
				'desc'      => '',
				'post_name' => '',
			)
		);

		// Assume the most generic term
		$noun = 'item';

		// Current site's timezone offset.
		$gmt_offset = get_option( 'gmt_offset' );

		// Use the noun `image` when sure the media file is an image
		if ( Utils\get_flag_value( $assoc_args, 'featured_image' ) || $assoc_args['alt'] ) {
			$noun = 'image';
		}

		$porcelain = Utils\get_flag_value( $assoc_args, 'porcelain' );
		if ( is_string( $porcelain ) && ! in_array( $porcelain, array( 'url' ), true ) ) {
			WP_CLI::error( sprintf( 'Invalid value for <porcelain>: %s. Expected flag or \'url\'.', $porcelain ) );
		}

		if ( isset( $assoc_args['post_id'] ) ) {
			if ( ! get_post( $assoc_args['post_id'] ) ) {
				WP_CLI::warning( 'Invalid --post_id' );
				$assoc_args['post_id'] = false;
			}
		} else {
			$assoc_args['post_id'] = false;
		}

		$number    = 0;
		$successes = 0;
		$errors    = 0;
		foreach ( $args as $file ) {
			++$number;
			if ( 0 === $number % self::WP_CLEAR_OBJECT_CACHE_INTERVAL ) {
				Utils\wp_clear_object_cache();
			}

			// phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url -- parse_url will only be used in absence of wp_parse_url.
			$is_file_remote = function_exists( 'wp_parse_url' ) ? wp_parse_url( $file, PHP_URL_HOST ) : parse_url( $file, PHP_URL_HOST );
			$orig_filename  = $file;
			$file_time      = '';

			if ( empty( $is_file_remote ) ) {
				if ( ! file_exists( $file ) ) {
					WP_CLI::warning( "Unable to import file '$file'. Reason: File doesn't exist." );
					++$errors;
					continue;
				}
				if ( Utils\get_flag_value( $assoc_args, 'skip-copy' ) ) {
					$tempfile = $file;
				} else {
					$tempfile = $this->make_copy( $file );
				}
				$name = Utils\basename( $file );

				if ( Utils\get_flag_value( $assoc_args, 'preserve-filetime' ) ) {
					$file_time = @filemtime( $file );
				}
			} else {
				$tempfile = download_url( $file );
				if ( is_wp_error( $tempfile ) ) {
					WP_CLI::warning(
						sprintf(
							"Unable to import file '%s'. Reason: %s",
							$file,
							implode( ', ', $tempfile->get_error_messages() )
						)
					);
					++$errors;
					continue;
				}
				$name = strtok( Utils\basename( $file ), '?' );
			}

			if ( ! empty( $assoc_args['file_name'] ) ) {
				$image_name = $this->get_image_name( $name, $assoc_args['file_name'] );
				$name       = ! empty( $image_name ) ? $image_name : $name;
			}

			$file_array = array(
				'tmp_name' => $tempfile,
				'name'     => $name,
			);

			$post_array = array(
				'post_title'   => $assoc_args['title'],
				'post_excerpt' => $assoc_args['caption'],
				'post_content' => $assoc_args['desc'],
				'post_name'    => $assoc_args['post_name'],
			);

			if ( ! empty( $file_time ) ) {
				$post_array['post_date']         = gmdate( 'Y-m-d H:i:s', $file_time + ( $gmt_offset * HOUR_IN_SECONDS ) );
				$post_array['post_date_gmt']     = gmdate( 'Y-m-d H:i:s', $file_time );
				$post_array['post_modified']     = gmdate( 'Y-m-d H:i:s', $file_time + ( $gmt_offset * HOUR_IN_SECONDS ) );
				$post_array['post_modified_gmt'] = gmdate( 'Y-m-d H:i:s', $file_time );
			}

			$post_array = wp_slash( $post_array );

			// use image exif/iptc data for title and caption defaults if possible
			if ( empty( $post_array['post_title'] ) || empty( $post_array['post_excerpt'] ) ) {
				// @codingStandardsIgnoreStart
				$image_meta = @wp_read_image_metadata( $tempfile );
				// @codingStandardsIgnoreEnd
				if ( ! empty( $image_meta ) ) {
					if ( empty( $post_array['post_title'] ) && trim( $image_meta['title'] ) && ! is_numeric( sanitize_title( $image_meta['title'] ) ) ) {
						$post_array['post_title'] = $image_meta['title'];
					}

					if ( empty( $post_array['post_excerpt'] ) && trim( $image_meta['caption'] ) ) {
						$post_array['post_excerpt'] = $image_meta['caption'];
					}
				}
			}

			if ( empty( $post_array['post_title'] ) ) {
				$post_array['post_title'] = preg_replace( '/\.[^.]+$/', '', Utils\basename( $file ) );
			}

			if ( Utils\get_flag_value( $assoc_args, 'skip-copy' ) ) {
				$wp_filetype                  = wp_check_filetype( $file, null );
				$post_array['post_mime_type'] = $wp_filetype['type'];
				$post_array['post_status']    = 'inherit';

				$success = wp_insert_attachment( $post_array, $file, $assoc_args['post_id'] );
				if ( is_wp_error( $success ) ) {
					WP_CLI::warning(
						sprintf(
							"Unable to insert file '%s'. Reason: %s",
							$orig_filename,
							implode( ', ', $success->get_error_messages() )
						)
					);
					++$errors;
					continue;
				}
				wp_update_attachment_metadata( $success, wp_generate_attachment_metadata( $success, $file ) );
			} else {
				// Deletes the temporary file.
				$success = media_handle_sideload( $file_array, $assoc_args['post_id'], $assoc_args['title'], $post_array );
				if ( is_wp_error( $success ) ) {
					WP_CLI::warning(
						sprintf(
							"Unable to import file '%s'. Reason: %s",
							$orig_filename,
							implode( ', ', $success->get_error_messages() )
						)
					);
					++$errors;
					continue;
				}
			}

			// Set alt text
			if ( $assoc_args['alt'] ) {
				update_post_meta( $success, '_wp_attachment_image_alt', wp_slash( $assoc_args['alt'] ) );
			}

			// Set as featured image, if --post_id and --featured_image are set
			if ( $assoc_args['post_id'] && Utils\get_flag_value( $assoc_args, 'featured_image' ) ) {
				update_post_meta( $assoc_args['post_id'], '_thumbnail_id', $success );
			}

			$attachment_success_text = '';
			if ( $assoc_args['file_name'] ) {
				$attachment_success_text .= " with file name {$name}";
			}

			if ( $assoc_args['post_id'] ) {
				$attachment_success_text = " and attached to post {$assoc_args['post_id']}";
				if ( Utils\get_flag_value( $assoc_args, 'featured_image' ) ) {
					$attachment_success_text .= ' as featured image';
				}
			}

			if ( $porcelain ) {
				if ( 'url' === strtolower( $porcelain ) ) {
					$file_location = $this->get_real_attachment_url( $success );
					WP_CLI::line( $file_location );
				} else {
					WP_CLI::line( $success );
				}
			} else {
				WP_CLI::log(
					sprintf(
						"Imported file '%s' as attachment ID %d%s.",
						$orig_filename,
						$success,
						$attachment_success_text
					)
				);
			}
			++$successes;
		}

		// Report the result of the operation
		if ( ! Utils\get_flag_value( $assoc_args, 'porcelain' ) ) {
			Utils\report_batch_operation_results( $noun, 'import', count( $args ), $successes, $errors );
		} elseif ( $errors ) {
			WP_CLI::halt( 1 );
		}
	}

	/**
	 * Lists image sizes registered with WordPress.
	 *
	 * ## OPTIONS
	 *
	 * [--fields=<fields>]
	 * : Limit the output to specific fields. Defaults to all fields.
	 *
	 * [--format=<format>]
	 * : Render output in a specific format
	 * ---
	 * default: table
	 * options:
	 *   - table
	 *   - json
	 *   - csv
	 *   - yaml
	 *   - count
	 * ---
	 *
	 * ## AVAILABLE FIELDS
	 *
	 * These fields will be displayed by default for each image size:
	 * * name
	 * * width
	 * * height
	 * * crop
	 * * ratio
	 *
	 * ## EXAMPLES
	 *
	 *     # List all registered image sizes
	 *     $ wp media image-size
	 *     +---------------------------+-------+--------+-------+-------+
	 *     | name                      | width | height | crop  | ratio |
	 *     +---------------------------+-------+--------+-------+-------+
	 *     | full                      |       |        | N/A   | N/A   |
	 *     | twentyfourteen-full-width | 1038  | 576    | hard  | 173:96|
	 *     | large                     | 1024  | 1024   | soft  | N/A   |
	 *     | medium_large              | 768   | 0      | soft  | N/A   |
	 *     | medium                    | 300   | 300    | soft  | N/A   |
	 *     | thumbnail                 | 150   | 150    | hard  | 1:1   |
	 *     +---------------------------+-------+--------+-------+-------+
	 *
	 * @subcommand image-size
	 */
	public function image_size( $args, $assoc_args ) {
		$assoc_args = array_merge(
			array(
				'fields' => 'name,width,height,crop,ratio',
			),
			$assoc_args
		);

		$sizes = $this->get_registered_image_sizes();

		usort(
			$sizes,
			function ( $a, $b ) {
				if ( $a['width'] === $b['width'] ) {
					return 0;
				}
				return ( $a['width'] < $b['width'] ) ? 1 : -1;
			}
		);
		array_unshift(
			$sizes,
			array(
				'name'   => 'full',
				'width'  => '',
				'height' => '',
				'crop'   => 'N/A',
				'ratio'  => 'N/A',
			)
		);
		WP_CLI\Utils\format_items( $assoc_args['format'], $sizes, explode( ',', $assoc_args['fields'] ) );
	}

	private function get_ratio( $width, $height ) {
		if ( 0 === $height ) {
			return "0:{$width}";
		}

		if ( 0 === $width ) {
			return "{$height}:0";
		}

		$gcd          = $this->gcd( $width, $height );
		$width_ratio  = $width / $gcd;
		$height_ratio = $height / $gcd;

		return "{$width_ratio}:{$height_ratio}";
	}

	private function gcd( $num1, $num2 ) {
		while ( 0 !== $num2 ) {
			$t    = $num1 % $num2;
			$num1 = $num2;
			$num2 = $t;
		}
		return $num1;
	}

	// wp_tempnam() inexplicably forces a .tmp extension, which spoils MIME type detection
	private function make_copy( $path ) {
		$dir      = get_temp_dir();
		$filename = Utils\basename( $path );
		if ( empty( $filename ) ) {
			$filename = time();
		}

		$filename = $dir . wp_unique_filename( $dir, $filename );
		if ( ! copy( $path, $filename ) ) {
			WP_CLI::error( "Could not create temporary file for $path." );
		}

		return $filename;
	}

	private function process_regeneration( $id, $skip_delete, $only_missing, $delete_unknown, $image_size, $progress, &$successes, &$errors, &$skips ) {

		$title = get_the_title( $id );
		if ( '' === $title ) {
			// If audio or video cover art then the id is the sub attachment id, which has no title.
			if ( metadata_exists( 'post', $id, '_cover_hash' ) ) {
				// Unfortunately the only way to get the attachment title would be to do a non-indexed query against the meta value of `_thumbnail_id`. So don't.
				$att_desc = sprintf( 'cover attachment (ID %d)', $id );
			} else {
				$att_desc = sprintf( '"(no title)" (ID %d)', $id );
			}
		} else {
			$att_desc = sprintf( '"%1$s" (ID %2$d)', $title, $id );
		}
		$thumbnail_desc = $image_size ? sprintf( '"%s" thumbnail', $image_size ) : 'thumbnail';

		$fullsizepath = $this->get_attached_file( $id );

		if ( false === $fullsizepath || ! file_exists( $fullsizepath ) ) {
			WP_CLI::warning( "Can't find $att_desc." );
			++$errors;
			return;
		}

		$is_pdf = 'application/pdf' === get_post_mime_type( $id );

		$original_meta = wp_get_attachment_metadata( $id );

		if ( $delete_unknown ) {
			$this->delete_unknown_image_sizes( $id, $fullsizepath );

			WP_CLI::log( "$progress Deleted unknown image sizes for $att_desc." );
			++$successes;
			return;
		}

		$needs_regeneration = $this->needs_regeneration( $id, $fullsizepath, $is_pdf, $image_size, $skip_delete, $skip_it );

		if ( $skip_it ) {
			WP_CLI::log( "$progress Skipped $thumbnail_desc regeneration for $att_desc." );
			++$skips;
			return;
		}

		if ( $only_missing && ! $needs_regeneration ) {
			WP_CLI::log( "$progress No $thumbnail_desc regeneration needed for $att_desc." );
			++$successes;
			return;
		}

		$metadata = wp_generate_attachment_metadata( $id, $fullsizepath );

		// Note it's possible for no metadata to be generated for PDFs if restricted to a specific image size.
		if ( empty( $metadata ) && ! ( $is_pdf && $image_size ) ) {
			WP_CLI::warning( sprintf( 'No metadata. (ID %d)', $id ) );
			WP_CLI::log( "$progress Couldn't regenerate thumbnails for $att_desc." );
			++$errors;
			return;
		}

		// On read error, we might only get the filesize returned and nothing else.
		if ( 1 === count( $metadata ) && array_key_exists( 'filesize', $metadata ) && ! ( $is_pdf && $image_size ) ) {
			WP_CLI::warning( sprintf( 'Read error while retrieving metadata. (ID %d)', $id ) );
			WP_CLI::log( "$progress Couldn't regenerate thumbnails for $att_desc." );
			++$errors;
			return;
		}

		if ( $image_size ) {
			if ( $this->update_attachment_metadata_for_image_size( $id, $metadata, $image_size, $original_meta ) ) {
				WP_CLI::log( "$progress Regenerated $thumbnail_desc for $att_desc." );
			} else {
				WP_CLI::log( "$progress No $thumbnail_desc regeneration needed for $att_desc." );
			}
		} else {
			wp_update_attachment_metadata( $id, $metadata );

			WP_CLI::log( "$progress Regenerated thumbnails for $att_desc." );
		}
		++$successes;
	}

	private function remove_old_images( $metadata, $fullsizepath, $image_size ) {

		if ( empty( $metadata['sizes'] ) ) {
			return;
		}

		if ( $image_size ) {
			if ( empty( $metadata['sizes'][ $image_size ] ) ) {
				return;
			}
			$metadata['sizes'] = array( $image_size => $metadata['sizes'][ $image_size ] );
		}

		$dir_path = dirname( $fullsizepath ) . '/';

		foreach ( $metadata['sizes'] as $size_info ) {
			$intermediate_path = $dir_path . $size_info['file'];

			if ( $intermediate_path === $fullsizepath ) {
				continue;
			}

			if ( file_exists( $intermediate_path ) ) {
				unlink( $intermediate_path );
			}
		}
	}

	private function needs_regeneration( $att_id, $fullsizepath, $is_pdf, $image_size, $skip_delete, &$skip_it ) {

		// Assume not skipping.
		$skip_it = false;

		// Note: zero-length string returned if no metadata, for instance if PDF or non-standard image (eg an SVG).
		$metadata = wp_get_attachment_metadata( $att_id );

		$image_sizes = $this->get_intermediate_image_sizes_for_attachment( $fullsizepath, $is_pdf, $metadata, $att_id );

		// First check if no applicable editor currently available (non-destructive - ie old thumbnails not removed).
		if ( is_wp_error( $image_sizes ) && 'image_no_editor' === $image_sizes->get_error_code() ) {
			// Warn unless PDF or non-standard image.
			if ( ! $is_pdf && is_array( $metadata ) && ! empty( $metadata['sizes'] ) ) {
				WP_CLI::warning( sprintf( '%s (ID %d)', $image_sizes->get_error_message(), $att_id ) );
			}
			$skip_it = true;
			return false;
		}

		// If uploaded when applicable image editor such as Imagick unavailable, the metadata or sizes metadata may not exist.
		if ( ! is_array( $metadata ) ) {
			$metadata = array();
		}
		// If set `$metadata['sizes']` should be array but explicitly check as following code depends on it.
		if ( ! isset( $metadata['sizes'] ) || ! is_array( $metadata['sizes'] ) ) {
			$metadata['sizes'] = array();
		}

		// Remove any old thumbnails (so now destructive).
		if ( ! $skip_delete ) {
			$this->remove_old_images( $metadata, $fullsizepath, $image_size );
		}

		// Check for any other error (such as load error) apart from no editor available.
		if ( is_wp_error( $image_sizes ) ) {
			// Warn but assume it may be possible to regenerate and allow processing to continue and possibly fail.
			WP_CLI::warning( sprintf( '%s (ID %d)', $image_sizes->get_error_message(), $att_id ) );
			return true;
		}

		// Have sizes - check whether they're new ones or they've changed. Note that an attachment can have no sizes if it's on or below the thumbnail threshold.

		if ( $image_size ) {
			if ( empty( $image_sizes[ $image_size ] ) ) {
				return false;
			}
			if ( empty( $metadata['sizes'][ $image_size ] ) ) {
				return true;
			}
			$metadata['sizes'] = array( $image_size => $metadata['sizes'][ $image_size ] );
		}

		if ( $this->image_sizes_differ( $image_sizes, $metadata['sizes'] ) ) {
			return true;
		}

		$dir_path = dirname( $fullsizepath ) . '/';

		// Check that the thumbnail files exist.
		foreach ( $metadata['sizes'] as $size_info ) {
			$intermediate_path = $dir_path . $size_info['file'];

			if ( $intermediate_path === $fullsizepath ) {
				continue;
			}

			if ( ! file_exists( $intermediate_path ) ) {
				return true;
			}
		}
		return false;
	}

	// Whether there's new image sizes or the width/height of existing image sizes have changed.
	private function image_sizes_differ( $image_sizes, $meta_sizes ) {
		// Check if have new image size(s).
		if ( array_diff( array_keys( $image_sizes ), array_keys( $meta_sizes ) ) ) {
			return true;
		}
		// Check if image sizes have changed.
		foreach ( $image_sizes as $name => $image_size ) {
			if ( $image_size['width'] !== $meta_sizes[ $name ]['width'] || $image_size['height'] !== $meta_sizes[ $name ]['height'] ) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Returns image sizes for a given attachment.
	 *
	 * Like WP's get_intermediate_image_sizes(), but removes sizes that won't be generated for a particular attachment due to it being on or below their thresholds,
	 * and returns associative array with size name => width/height entries, resolved to crop values if applicable.
	 *
	 * @param string $fullsizepath Filepath of the attachment
	 * @param bool   $is_pdf       Whether it is a PDF.
	 * @param array  $metadata     Attachment metadata.
	 * @param int    $att_id       Attachment ID.
	 *
	 * @return array|WP_Error Image sizes on success, WP_Error instance otherwise.
	 */
	private function get_intermediate_image_sizes_for_attachment( $fullsizepath, $is_pdf, $metadata, $att_id ) {

		// Need to get width, height of attachment for image_resize_dimensions().
		$editor = wp_get_image_editor( $fullsizepath );
		if ( is_wp_error( $editor ) ) {
			return $editor;
		}
		$result = $editor->load();
		if ( is_wp_error( $result ) ) {
			unset( $editor );
			return $result;
		}
		list( $width, $height ) = array_values( $editor->get_size() );
		unset( $editor );

		$sizes = array();
		foreach ( $this->get_intermediate_sizes( $is_pdf, $metadata, $att_id ) as $name => $size ) {
			// Need to check destination and original width or height differ before calling image_resize_dimensions(), otherwise it will return non-false.
			$dims = image_resize_dimensions( $width, $height, $size['width'], $size['height'], $size['crop'] );
			if ( ( $width !== $size['width'] || $height !== $size['height'] ) && $dims ) {
				list( $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h ) = $dims;
				$sizes[ $name ] = array(
					'width'  => $dst_w,
					'height' => $dst_h,
				);
			}
		}
		return $sizes;
	}

	// Like WP's get_intermediate_image_sizes(), but returns associative array with name => size info entries (and caters for PDFs also).
	private function get_intermediate_sizes( $is_pdf, $metadata, $att_id ) {
		if ( $is_pdf ) {
			// Copied from wp_generate_attachment_metadata() in "wp-admin/includes/image.php".
			$fallback_sizes = array(
				'thumbnail',
				'medium',
				'large',
			);
			// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Calling native WordPress hook.
			$intermediate_image_sizes = apply_filters( 'fallback_intermediate_image_sizes', $fallback_sizes, $metadata );
		} else {
			$intermediate_image_sizes = get_intermediate_image_sizes();
		}

		// Adapted from wp_generate_attachment_metadata() in "wp-admin/includes/image.php".

		if ( function_exists( 'wp_get_additional_image_sizes' ) ) {
			$_wp_additional_image_sizes = wp_get_additional_image_sizes();
		} else {
			// For WP < 4.7.0.
			global $_wp_additional_image_sizes;
			if ( ! $_wp_additional_image_sizes ) {
				// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Used as a fallback for WordPress version less than 4.7.0 as function wp_get_additional_image_sizes didn't exist then.
				$_wp_additional_image_sizes = array();
			}
		}

		$sizes = array();
		foreach ( $intermediate_image_sizes as $s ) {
			if ( isset( $_wp_additional_image_sizes[ $s ]['width'] ) ) {
				$sizes[ $s ]['width'] = (int) $_wp_additional_image_sizes[ $s ]['width'];
			} else {
				$sizes[ $s ]['width'] = (int) get_option( "{$s}_size_w" );
			}

			if ( isset( $_wp_additional_image_sizes[ $s ]['height'] ) ) {
				$sizes[ $s ]['height'] = (int) $_wp_additional_image_sizes[ $s ]['height'];
			} else {
				$sizes[ $s ]['height'] = (int) get_option( "{$s}_size_h" );
			}

			if ( isset( $_wp_additional_image_sizes[ $s ]['crop'] ) ) {
				$sizes[ $s ]['crop'] = (bool) $_wp_additional_image_sizes[ $s ]['crop'];
				// Force PDF thumbnails to be soft crops.
			} elseif ( $is_pdf && 'thumbnail' === $s ) {
				$sizes[ $s ]['crop'] = false;
			} else {
				$sizes[ $s ]['crop'] = (bool) get_option( "{$s}_crop" );
			}
		}

		// Check here that not PDF (as filter not applied in core if is) and `$metadata` is array (as may not be and filter only applied in core when is).
		if ( ! $is_pdf && is_array( $metadata ) ) {
			if ( Utils\wp_version_compare( '5.3', '>=' ) ) {
				// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Calling native WordPress hook.
				$sizes = apply_filters( 'intermediate_image_sizes_advanced', $sizes, $metadata, $att_id );
			} else {
				// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Calling native WordPress hook.
				$sizes = apply_filters( 'intermediate_image_sizes_advanced', $sizes, $metadata );
			}
		}

		return $sizes;
	}

	// Add filters to only process a particular intermediate image size in wp_generate_attachment_metadata().
	private function add_image_size_filters( $image_size ) {
		$image_size_filters = array();

		// For images.
		$image_size_filters['intermediate_image_sizes_advanced'] = function ( $sizes ) use ( $image_size ) {
			// $sizes is associative array of name => size info entries.
			if ( isset( $sizes[ $image_size ] ) ) {
				return array( $image_size => $sizes[ $image_size ] );
			}
			return array();
		};

		// For PDF previews.
		$image_size_filters['fallback_intermediate_image_sizes'] = function ( $fallback_sizes ) use ( $image_size ) {
			// $fallback_sizes is indexed array of size names.
			if ( in_array( $image_size, $fallback_sizes, true ) ) {
				return array( $image_size );
			}
			return array();
		};

		foreach ( $image_size_filters as $name => $filter ) {
			add_filter( $name, $filter, PHP_INT_MAX );
		}

		return $image_size_filters;
	}

	// Remove above intermediate image size filters.
	private function remove_image_size_filters( $image_size_filters ) {
		foreach ( $image_size_filters as $name => $filter ) {
			remove_filter( $name, $filter, PHP_INT_MAX );
		}
	}

	// Update attachment sizes metadata just for a particular intermediate image size.
	private function update_attachment_metadata_for_image_size( $id, $new_metadata, $image_size, $metadata ) {

		if ( ! is_array( $metadata ) ) {
			return false;
		}

		// If have metadata for image_size.
		if ( ! empty( $new_metadata['sizes'][ $image_size ] ) ) {
			$metadata['sizes'][ $image_size ] = $new_metadata['sizes'][ $image_size ];
			wp_update_attachment_metadata( $id, $metadata );
			return true;
		}

		// Else remove unused metadata if any.
		if ( ! empty( $metadata['sizes'][ $image_size ] ) ) {
			unset( $metadata['sizes'][ $image_size ] );
			wp_update_attachment_metadata( $id, $metadata );
			// Treat removing unused metadata as no change.
		}
		return false;
	}

	/**
	 * Get images from the installation.
	 *
	 * @param array $args                  The query arguments to use. Optional.
	 * @param array $additional_mime_types The additional mime types to search for. Optional.
	 *
	 * @return WP_Query The query result.
	 */
	private function get_images( $args = array(), $additional_mime_types = array() ) {
		$mime_types = array_merge( array( 'image' ), $additional_mime_types );

		$query_args = array(
			'post_type'              => 'attachment',
			'post__in'               => $args,
			'post_mime_type'         => $mime_types,
			'post_status'            => 'any',
			'posts_per_page'         => -1,
			'fields'                 => 'ids',
			'update_post_meta_cache' => false,
			'update_post_term_cache' => false,
		);

		return new WP_Query( $query_args );
	}

	/**
	 * Get all the registered image sizes along with their dimensions.
	 *
	 * @return array $image_sizes The image sizes
	 */
	private function get_registered_image_sizes() {
		$image_sizes = array();

		$all_sizes = $this->wp_get_registered_image_subsizes();

		foreach ( $all_sizes as $size => $size_args ) {
			$crop = filter_var( $size_args['crop'], FILTER_VALIDATE_BOOLEAN );

			$image_sizes[] = array(
				'name'   => $size,
				'width'  => $size_args['width'],
				'height' => $size_args['height'],
				'crop'   => empty( $crop ) || is_array( $size_args['crop'] ) ? 'soft' : 'hard',
				'ratio'  => empty( $crop ) || is_array( $size_args['crop'] ) ? 'N/A' : $this->get_ratio( $size_args['width'], $size_args['height'] ),
			);
		}

		return $image_sizes;
	}

	/**
	* Returns a normalized list of all currently registered image sub-sizes.
	*
	* If exists, uses output of wp_get_registered_image_subsizes() function (introduced in WP 5.3).
	* Definition of this method is modified version of core function wp_get_registered_image_subsizes().
	*
	* @global array $_wp_additional_image_sizes
	*
	* @return array[] Associative array of arrays of image sub-size information, keyed by image size name.
	*/
	private function wp_get_registered_image_subsizes() {
		if ( Utils\wp_version_compare( '5.3', '>=' ) ) {
			return wp_get_registered_image_subsizes();
		}

		global $_wp_additional_image_sizes;

		$additional_sizes = $_wp_additional_image_sizes ? $_wp_additional_image_sizes : array();

		$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;
	}

	/**
	 * Fix image orientation for one or more attachments.
	 *
	 * ## OPTIONS
	 *
	 * [<attachment-id>...]
	 * : One or more IDs of the attachments to regenerate.
	 *
	 * [--dry-run]
	 * : Check images needing orientation without performing the operation.
	 *
	 * ## EXAMPLES
	 *
	 *     # Fix orientation for all images.
	 *     $ wp media fix-orientation
	 *     1/3 Fixing orientation for "Landscape_4" (ID 62).
	 *     2/3 Fixing orientation for "Landscape_3" (ID 61).
	 *     3/3 Fixing orientation for "Landscape_2" (ID 60).
	 *     Success: Fixed 3 of 3 images.
	 *
	 *     # Fix orientation dry run.
	 *     $ wp media fix-orientation 63 --dry-run
	 *     1/1 "Portrait_6" (ID 63) will be affected.
	 *     Success: 1 of 1 image will be affected.
	 *
	 *     # Fix orientation for specific images.
	 *     $ wp media fix-orientation 63
	 *     1/1 Fixing orientation for "Portrait_6" (ID 63).
	 *     Success: Fixed 1 of 1 images.
	 *
	 * @subcommand fix-orientation
	 */
	public function fix_orientation( $args, $assoc_args ) {

		// EXIF is required to read image metadata for orientation.
		if ( ! extension_loaded( 'exif' ) ) {
			WP_CLI::error( "'EXIF' extension is not loaded, it is required for this operation." );
		} elseif ( ! function_exists( 'exif_read_data' ) ) {
			WP_CLI::error( "Function 'exif_read_data' does not exist, it is required for this operation." );
		}

		$images  = $this->get_images( $args );
		$count   = $images->post_count;
		$dry_run = Utils\get_flag_value( $assoc_args, 'dry-run' );

		if ( ! $count ) {
			WP_CLI::error( 'No images found.' );
		}

		$number    = 0;
		$successes = 0;
		$errors    = 0;
		foreach ( $images->posts as $post_id ) {
			++$number;
			if ( 0 === $number % self::WP_CLEAR_OBJECT_CACHE_INTERVAL ) {
				Utils\wp_clear_object_cache();
			}
			$this->process_orientation_fix( $post_id, "{$number}/{$count}", $successes, $errors, $dry_run );
		}

		if ( Utils\get_flag_value( $assoc_args, 'dry-run' ) ) {
			WP_CLI::success( sprintf( '%s of %s %s will be affected.', $successes, $count, Utils\pluralize( 'image', $count ) ) );
		} else {
			Utils\report_batch_operation_results( 'image', 'fix', $count, $successes, $errors );
		}
	}

	/**
	 * Perform orientation fix on attachments.
	 *
	 * @param int    $id        Attachment Id.
	 * @param string $progress  Current progress string.
	 * @param int    $successes Count of success in current operation.
	 * @param int    $errors    Count of errors in current operation.
	 * @param bool   $dry_run   Is this a dry run?
	 */
	private function process_orientation_fix( $id, $progress, &$successes, &$errors, $dry_run ) {
		$title = get_the_title( $id );
		if ( '' === $title ) {
			// If audio or video cover art then the id is the sub attachment id, which has no title.
			if ( metadata_exists( 'post', $id, '_cover_hash' ) ) {
				// Unfortunately the only way to get the attachment title would be to do a non-indexed query against the meta value of `_thumbnail_id`. So don't.
				$att_desc = sprintf( 'cover attachment (ID %d)', $id );
			} else {
				$att_desc = sprintf( '"(no title)" (ID %d)', $id );
			}
		} else {
			$att_desc = sprintf( '"%1$s" (ID %2$d)', $title, $id );
		}

		$full_size_path = $this->get_attached_file( $id );

		if ( false === $full_size_path || ! file_exists( $full_size_path ) ) {
			WP_CLI::warning( "Can't find {$att_desc}." );
			++$errors;
			return;
		}

		// Get current metadata of the attachment.
		$metadata   = wp_generate_attachment_metadata( $id, $full_size_path );
		$image_meta = ! empty( $metadata['image_meta'] ) ? $metadata['image_meta'] : [];

		if ( isset( $image_meta['orientation'] ) && absint( $image_meta['orientation'] ) > 1 ) {
			if ( ! $dry_run ) {
				WP_CLI::log( "{$progress} Fixing orientation for {$att_desc}." );
				if ( false !== $this->flip_rotate_image( $id, $metadata, $image_meta, $full_size_path ) ) {
					++$successes;
				} else {
					++$errors;
					WP_CLI::log( "Couldn't fix orientation for {$att_desc}." );
				}
			} else {
				WP_CLI::log( "{$progress} {$att_desc} will be affected." );
				++$successes;
			}
		} else {
			WP_CLI::log( "{$progress} No orientation fix required for {$att_desc}." );
		}
	}

	/**
	 * Perform image rotate operations on the image.
	 *
	 * @param int    $id             Attachment Id.
	 * @param array  $metadata       Attachment Metadata.
	 * @param array  $image_meta     `image_meta` information for the attachment.
	 * @param string $full_size_path Path to original image.
	 *
	 * @return bool Whether the image rotation operation succeeded.
	 */
	private function flip_rotate_image( $id, $metadata, $image_meta, $full_size_path ) {
		$editor = wp_get_image_editor( $full_size_path );

		if ( ! is_wp_error( $editor ) ) {
			$operations = $this->calculate_transformation( (int) $image_meta['orientation'] );

			// Rotate image if required.
			if ( true === $operations['rotate'] ) {
				$editor->rotate( $operations['degree'] );
			}

			// Flip image if required.
			if ( false !== $operations['flip'] ) {
				$editor->flip( $operations['flip'][0], $operations['flip'][1] );
			}

			// Save the image and generate metadata.
			$editor->save( $full_size_path );
			$metadata   = wp_generate_attachment_metadata( $id, $full_size_path );
			$image_meta = empty( $metadata['image_meta'] ) ? [] : $metadata['image_meta'];

			// Update attachment metadata with newly generated data.
			wp_update_attachment_metadata( $id, $metadata );

			if ( isset( $image_meta['orientation'] ) && absint( $image_meta['orientation'] ) === 0 ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Return array of operations to be done for provided orientation value.
	 *
	 * @param int $orientation EXIF orientation value.
	 *
	 * @return array
	 */
	private function calculate_transformation( $orientation ) {
		$rotate = false;
		$flip   = false;
		$degree = 0;
		switch ( $orientation ) {
			case 2:
				$flip = [ false, true ]; // $flip image along given axis [ horizontal, vertical ]
				break;
			case 3:
				$flip = [ true, true ];
				break;
			case 4:
				$flip = [ true, false ];
				break;
			case 5:
				$degree = -90;
				$rotate = true;
				$flip   = [ false, true ];
				break;
			case 6:
				$degree = -90;
				$rotate = true;
				break;
			case 7:
				$degree = 90;
				$rotate = true;
				$flip   = [ false, true ];
				break;
			case 8:
				$degree = 90;
				$rotate = true;
				break;
			default:
				$degree = 0;
				$rotate = true;
				break;
		}

		return [
			'flip'   => $flip,
			'degree' => $degree,
			'rotate' => $rotate,
		];
	}

	/**
	 * Add compatibility indirection to get_attached_file().
	 *
	 * In WordPress 5.3, behavior changed to account for automatic resizing of
	 * big image files.
	 *
	 * @see https://core.trac.wordpress.org/ticket/47873
	 *
	 * @param int $attachment_id ID of the attachment to get the filepath for.
	 * @return string|false Filepath of the attachment, or false if not found.
	 */
	private function get_attached_file( $attachment_id ) {
		if ( function_exists( 'wp_get_original_image_path' ) ) {
			$filepath = wp_get_original_image_path( $attachment_id );

			if ( false !== $filepath ) {
				return $filepath;
			}
		}

		return get_attached_file( $attachment_id );
	}

	/**
	 * Image-friendly alternative to wp_get_attachment_url(). Will return the full size URL of an image instead of the `-scaled` version.
	 *
	 * In WordPress 5.3, behavior changed to account for automatic resizing of
	 * big image files.
	 *
	 * @see https://core.trac.wordpress.org/ticket/47873
	 *
	 * @param int $attachment_id ID of the attachment to get the URL for.
	 * @return string|false URL of the attachment, or false if not found.
	 */
	private function get_real_attachment_url( $attachment_id ) {
		if ( function_exists( 'wp_get_original_image_url' ) ) {
			$url = wp_get_original_image_url( $attachment_id );

			if ( false !== $url ) {
				return $url;
			}
		}

		return wp_get_attachment_url( $attachment_id );
	}

	/**
	 * Create image slug based on user input slug.
	 * Add basename extension to slug.
	 *
	 * @param string $basename Default slu of image.
	 * @param string $slug User input slug.
	 *
	 * @return string Image slug with extension.
	 */
	private function get_image_name( $basename, $slug ) {

		$extension = pathinfo( $basename, PATHINFO_EXTENSION );

		return $slug . '.' . $extension;
	}

	/**
	 * Removes files for unknown/unregistered image sizes.
	 *
	 * Similar to {@see self::remove_old_images} but also updates metadata afterwards.
	 *
	 * @param int    $id           Attachment ID.
	 * @param string $fullsizepath Filepath of the attachment.
	 *
	 * @return void
	 */
	private function delete_unknown_image_sizes( $id, $fullsizepath ) {
		$original_meta = wp_get_attachment_metadata( $id );

		$image_sizes = wp_list_pluck( $this->get_registered_image_sizes(), 'name' );

		$dir_path = dirname( $fullsizepath ) . '/';

		$sizes_to_delete = array();

		if ( isset( $original_meta['sizes'] ) ) {
			foreach ( $original_meta['sizes'] as $size_name => $size_meta ) {
				if ( 'full' === $size_name ) {
					continue;
				}

				if ( ! in_array( $size_name, $image_sizes, true ) ) {
					$intermediate_path = $dir_path . $size_meta['file'];
					if ( $intermediate_path === $fullsizepath ) {
						continue;
					}

					if ( file_exists( $intermediate_path ) ) {
						unlink( $intermediate_path );
					}

					$sizes_to_delete[] = $size_name;
				}
			}

			foreach ( $sizes_to_delete as $size_name ) {
				unset( $original_meta['sizes'][ $size_name ] );
			}
		}

		wp_update_attachment_metadata( $id, $original_meta );
	}
}
